Skip to main content

Signup — tell us who your user is

You already have users. Before one of them can hold money, we need to know who they are, because the identification obligation is ours as the e-money issuer.

Three gates, in order:

Step 1 — Register the subject

PUT /subjects/drv-10021
Authorization: Bearer <your M2M token>
X-Tenant-Code: ACME_RIDES
Idempotency-Key: 9f2b1c44-...
Content-Type: application/json

{
"fullName": "Aisyah binti Rahman",
"email": "aisyah@example.com",
"phone": "+60123456789",
"locale": "ms"
}

drv-10021 is your identifier for this person. Use it in every later call — you never have to store ours, though we return it. PUT is deliberate: replaying it is free, so a retry after a timeout is safe.

Read the state back at any time with GET /subjects/drv-10021.

Step 2 — Prove the contact details

Registering an email is not the same as proving it. A registered contact is tenant-asserted; it becomes platform-verified only when a code we sent to that destination comes back to us.

POST /otp/challenges
{ "externalUserRef": "drv-10021", "purpose": "CONTACT_VERIFY", "channel": "EMAIL" }
{ "data": { "challengeId": "…", "maskedDestination": "a•••••@example.com",
"ttlSeconds": 300, "resendAfterSeconds": 60 } }

Render your own code-entry screen using maskedDestination — "we sent a code to a•••••@example.com". When the user types it:

POST /otp/challenges/{challengeId}/verify
{ "code": "418239" }

There is deliberately no destination field on the challenge request. We resolve it from the subject's registered contact, so you cannot redirect a code to yourself. That is what makes a returned code evidence about the person rather than evidence about you.

Handle these:

Wrong code400 with attemptsRemaining
Too many attemptsThe challenge dies. Issue a new one
Resend inside the cooldown429 — show the countdown from resendAfterSeconds
SMS is not built

channel: "SMS" is accepted by the schema and returns 501. There is no SMS transport in the platform. It is blocked on MCMC sender-ID registration, which has lead time we do not control.

A phone number can be registered now; it cannot be verified until that ships. If your launch requires a verified handset, tell us early — it is a procurement date, not an engineering estimate.

Step 3 — Read them back

There is no separate "create the wallet user" call. The subject you registered in step 1 is the wallet user — one row, one identity. Posting to POST /users with the same email afterwards is refused with a 400 saying the user already exists, which is the system telling you the work is already done.

GET /users/{id}            → the profile
GET /users/{id}/overview → profile plus wallet summary

A CASH wallet was opened alongside them. See step 2 of the walkthrough and Open the wallet for when you still need to open one yourself.

Step 4 — Submit KYC evidence

PUT /users/{id}/kyc
Idempotency-Key: <uuid>

{
"idType": "MYKAD",
"idNumber": "900101145678",
"dateOfBirth": "1990-01-01",
"nationality": "Malaysian",
"address": "No. 1, Jalan Ampang, 50450 Kuala Lumpur"
}

The record always lands PENDING. A GoHubPay reviewer adjudicates it separately, on POST /users/{id}/kyc/decision — an endpoint you cannot call; it returns 403 for tenant tokens. It is documented so you can see where the decision happens and what it produces, not so you can invoke it.

status is not yours to set

Sending status on PUT /users/{id}/kyc returns 403. It is refused rather than ignored, so you find out immediately rather than believing you verified someone.

We are the e-money issuer. If you could write VERIFIED, the record would capture your assertion rather than our decision, and the control that gates money leaving the platform would be decorative. A tenant's own review of this API made exactly that argument, and they were right.

Re-submitting evidence returns the record to PENDING, including when it was already VERIFIED — a decision made against one set of documents does not carry over to a different set. This only ever downgrades, so it is safe for you to call, but design for it: correcting a typo in an address will require re-adjudication.

What is true afterwards

CheckExpect
GET /subjects/drv-10021the subject exists, contacts show verified: true for EMAIL
GET /users/{id}/kycstatus: "PENDING", verifiedAt: null
Attempt a withdrawalrefused until a reviewer sets VERIFIED — see Money out

Next: Open the wallet.