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 code | 400 with attemptsRemaining |
| Too many attempts | The challenge dies. Issue a new one |
| Resend inside the cooldown | 429 — show the countdown from resendAfterSeconds |
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 setSending 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
| Check | Expect |
|---|---|
GET /subjects/drv-10021 | the subject exists, contacts show verified: true for EMAIL |
GET /users/{id}/kyc | status: "PENDING", verifiedAt: null |
| Attempt a withdrawal | refused until a reviewer sets VERIFIED — see Money out |
Next: Open the wallet.