Onboarding
The lifecycle of a connection, and what freezes the moment a merchant accepts it.
A connection carries two statuses that move independently. status says whether the merchant has
accepted, payment_status says whether the connection may move money. A live connection can be
active and still refuse every payment, which is the single most common surprise in this section.
The lifecycle
status | What it means | How it got there |
|---|---|---|
pending | Created, nobody has accepted | POST /accounts |
active | A merchant signed in and authorised it | They opened the authorisation link |
cancelled | Ended, by you | DELETE /accounts/{id} |
expired | The invitation ran out before anyone accepted | Time |
There is no restricted and no intermediate state. A cancelled connection reads cancelled, one
with a merchant attached reads active, an invitation nobody accepted before its date reads
expired, and everything else reads pending.
expires_at is the invitation's date: it is cleared when a merchant accepts, and an accepted
connection never expires.
The authorisation link
Creating a connection mints a single-use authorisation token and hands it back inside a URL.
The merchant opens it, signs in to their own Wajub account, sees the capabilities you asked for, and accepts. Your platform never handles their credentials and never creates anything on their side.
The link is a credential
Anyone holding that URL can attach an account to your platform. Send it over a channel you
control, and treat it like a secret. If it leaks, POST /accounts/{id}/token revokes every link
already issued, a reminder's included, and mints a replacement, valid for 30 days again. The call
is refused with 400 once a merchant has claimed the connection, whether it is still running or
was disconnected, because that merchant stays attached to it and the new link could never be used.
Reaching them again means a new connection they accept again.
An invitation does not stay open forever: it expires 30 days after its link was last minted, by
creating the connection, by POST /accounts/{id}/token or by Resend invitation in the
Dashboard, each of which restarts the 30 days. The last two revoke the links already sent.
Three days before that, a reminder goes to the merchant's address if you gave one, with email on
POST /accounts or PUT /accounts/{id}; it is kept in profile.email. The reminder carries a link
of its own, and the link you already sent keeps working. Once the invitation expires, every link
in the merchant's inbox stops working. A Relay connection has no invitation, no authorization link
(authorization_url is null) and never expires.
You can hold up to 50 open invitations per environment: connections created and not yet accepted,
cancelled or expired. Relay connections do not count, since no merchant ever accepts them. Past
the cap, POST /accounts answers 429 with You have reached the maximum of 50 pending invitations. Cancel the invitations you no longer need, or let them expire.
You see authorization_url once, in the answer to the call that minted it: creating the subaccount,
or regenerating its token. Reading the subaccount afterwards never returns it, claimed or not. Keep
it when you receive it; if you lose it, mint a new one. Once the connection is claimed, slave
appears in the response, carrying the merchant's business name and email.
What the link cannot do
A connection is claimed once, and the checks run at the moment of the claim, not when the merchant opened the link:
- a connection you cancelled stays cancelled, and the link cannot revive it.
POST /accounts/{id}/tokencan: on a cancelled invitation nobody claimed, it mints a new link and puts the connection back topending, with 30 days again; - a connection another merchant already claimed cannot be taken over;
- an expired invitation is refused;
- in live mode, the merchant cannot claim it before filing their KYC. Filing is enough to connect; verification is what releases payments (below);
- the link can carry a
redirect_uri, but only one sharing an origin with thecallbackregistered on the connection. Any other host is refused, and a connection with no callback accepts none.
A merchant who submits twice lands on their own finished connection, not on an error.
Back on your page
Before you send the link, you can add &state=… (a value of yours, such as a random token tied to
the vendor's session) and &redirect_uri=… to it. Once the merchant has accepted, they are sent to
that redirect_uri, or to the connection's callback when the link carries none, with these query
parameters:
| Parameter | What it carries |
|---|---|
account | The connection's id (acc_…), the one every call and every account.* event uses |
state | The state you put on the authorisation link, returned as it was. Check it against the one you stored, to know the visit is the answer to your own link |
status | connected |
https://example.com/connected?account=acc_7Yh2MpL4tRb3nP8sZcXv&state=8f3a&status=connectedThe callback must be an HTTPS address: POST and PUT /accounts answer 422 otherwise, except
http://localhost and http://127.0.0.1 with a sandbox key.
The return is a convenience for the merchant's browser, not a proof: anyone can open that URL.
Confirm the connection with GET /accounts/{account} or wait for account.connected on your
webhook endpoint before you act on it.
Then payments have to be switched on
Acceptance is not always permission to charge. payment_status is set the moment the merchant
accepts.
| The merchant accepts | payment_status |
|---|---|
| In sandbox | active straight away |
| In live, with their verification already cleared | active straight away, provided a lite connection also has the Wajub rail open |
| In live, with their verification still under review | inactive |
An inactive connection turns active on its own when the merchant's verification clears, and, on
a lite connection, once the Wajub rail is granted to them. Wajub then sends
account.payment_activated.
payment_status | Effect on requests carrying X-Sync |
|---|---|
inactive | Calls that move money refused with 403; reads run |
active | Everything the capabilities allow runs |
suspended | Calls that move money refused with 403; reads run |
What is refused while payment_status is not active: creating, activating, updating or charging a
payment, creating or changing a payment link, a refund, a transfer or a batch, adding, changing or
verifying a beneficiary or recording what a seller owes back, and accepting or closing a dispute.
What still works: every GET, so you can follow what the connection did while the merchant's
verification is under review, plus cancelling a payment, deleting a payment link or a beneficiary,
and the identity lookups (/identity/resolve, /identity/validate).
Suspension is yours to use afterwards. Setting payment_status to suspended stops inbound
payments and outbound transfers together, deliberately: a connection frozen for a failed compliance
recheck should not be able to drain its balance while it is unable to earn.
The switch only lifts a suspension you applied yourself. Setting payment_status back to active
answers 409 with error_code: merchant_not_verified while the merchant's verification is pending
or was rejected: those connections come back on their own once verification clears.
A Lite seller's own portal
A lite seller has no merchant dashboard, which is the point of the mode. Their onboarding ends on
the Wajub rail application rather than on a dashboard, and afterwards they use a portal of their
own, carrying your branding.
| They do | Where |
|---|---|
| Sign in | A one-time code sent to the shop's email address, with no password and a short session |
| See their sales and payouts | Only those carrying this connection: never their other business, never another platform's |
| Apply for the Wajub rail | Hosted, during onboarding; the connection cannot charge until it is approved |
| Declare where their money goes | The portal's payout account page |
A new payout destination is held before it can be used
A destination the seller has just declared or changed cannot receive a payout during a cooling-off period; the portal tells the seller when payouts to it can start. A transfer to a held destination is refused until the period is over.
What the merchant has to agree to again
The moment a merchant accepts, two of the four editable fields stop being yours alone.
| Field | After acceptance |
|---|---|
capabilities | 202 Accepted: proposed to the merchant, returned in pending_changes |
pricing | 202 Accepted: proposed to the merchant, returned in pending_changes |
callback | Still editable outright |
payment_status | Still editable outright, and only editable from here |
The asymmetry is deliberate. What the merchant agreed to on the authorisation screen cannot be changed behind their back, so a wider connection or a higher commission is a proposal they accept or decline in their own dashboard, and the connection keeps running on its current terms in the meantime. Suspension stays yours, with no proposal needed.
On their side, the merchant finds every platform they are connected to under Settings, Connected
apps: the capabilities they granted, the commission they agreed to, whether payments are on, and
any proposal waiting for them. Accepting or declining a proposal there sends you
account.change_accepted or account.change_declined. The same page lets them disconnect from your
platform, which ends the connection exactly as your own DELETE would.
Ending a connection
DELETE /accounts/{id} revokes the tokens, sets status to cancelled, and fires
account.deauthorized. If the merchant had accepted, payment_status drops to inactive at the
same time and the response reads Account disconnected; on an unclaimed invitation it reads
Account cancelled.
curl -X DELETE https://api.wajub.com/accounts/acc_7Yh2MpL4tRb3nP8sZcXv \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…"Disconnecting does not touch anything that already happened. The merchant keeps their account, their payments and their balance, and the commissions you already earned stay earned. What stops is your ability to act on their behalf.
The events to listen for
A connection's life is told by the events below, on your platform's endpoints. Creating a
connection sends account.created, with the account but not its link: authorization_url is only in
the answer to POST /accounts. The event catalogue
describes every one of them.
| Event | Sent when |
|---|---|
account.created | You create it with POST /accounts |
account.onboarding_started | The merchant opens the authorisation link |
account.onboarding_email_verified | They confirm their email address |
account.onboarding_account_selected | They pick the Wajub account to connect |
account.onboarding_kyc_submitted | They file their KYC during the onboarding |
account.connected | They accept the connection |
account.payment_activated | It becomes able to charge |
account.payment_suspended | It is suspended, by you or after a failed verification |
account.change_requested | You propose new capabilities or pricing |
account.change_accepted | The merchant accepts a proposal |
account.change_declined | The merchant declines a proposal |
account.updated | It changes: through PUT /accounts/{id} (not a proposal alone, not a call that changes nothing), when POST /accounts/{id}/token puts a cancelled invitation back to pending, or when Wajub sends a Lite merchant's payout (last_payout_at) |
account.commission_receivable_recorded | A commission is owed on a sale collected outside Wajub |
account.commission_receivable_collected | That commission is collected |
account.deauthorized | It is disconnected or cancelled |
Two formats, two signatures
The events that follow your own API calls (POST /accounts, PUT and DELETE /accounts/{id},
POST /accounts/{id}/token, Lite payouts, change requests, commission receivables) carry the
usual envelope, X-Wajub-Signature and X-Wajub-Timestamp. The events of the merchant's own
steps (onboarding_*, connected, change_accepted, change_declined, the activations that
follow a verification, and a disconnection made in the Dashboard) carry an X-Sync-Event header
and a flat body with event, account (the acc_… id, also in sync_uid for older
integrations), sandbox, timestamp and the event's own fields. They are signed in
X-Sync-Signature-V1 and X-Sync-Timestamp, the same way as the others: v1= and the hex
HMAC-SHA256 of {timestamp}.{raw_body} with your endpoint's signing secret. Pass those two headers
to the SDK constructEvent, which checks the signature and refuses a timestamp more than 300
seconds old. Then keep the X-Idempotency-Key of every delivery you processed and ignore one you
have already seen. They also carry X-Sync-Signature, the hex HMAC-SHA256 of the raw body alone,
kept for the integrations that verify it today and to be retired later: do not build on it. These
events reach only an endpoint subscribed to their exact name, not one subscribed to *, and get
three attempts: at once, then 30 seconds and 2 minutes later.
On top of those, every payment, refund and transfer made on a connected account, and every dispute on one of its payments, fires its normal webhook to the merchant and a copy to your platform. You do not need to poll a connection to know what it is doing.