Coinland PayDocs

Customer identity binding

Why every session names your customer, the binding ceremony, the review queue, and the three endpoints that manage a binding

Every checkout session belongs to one of your customers, and Coinland Pay makes that relationship explicit and permanent: the first time a customer of yours pays, the Coinland account they pay with is bound to your customer.id. From then on, only that Coinland account can pay sessions for that customer, only you can sever the link, and the customer cannot change it themselves.

This is an AML requirement, not an optimisation. A payment on this rail is always traceable to a named customer of a named business, and the binding is what makes "the same person" a fact rather than a guess.

The customer block

POST /sessions requires a customer object on every call. There is no opt-out and no per-merchant exemption: a create call without it is refused with PAY_CUSTOMER_REQUIRED (422).

"customer": {
  "id": "user-1042",
  "name": "Hamid Khosravi",
  "native_name": "حمید خسروی",
  "email": "hamid@example.com"
}
FieldRequiredConstraintsWhat it is
idYes[A-Za-z0-9_.:-], 1-128Your own internal user id for this customer. The binding is keyed on it forever, so it must be stable in your system
nameYes1-255The customer's full name, in Latin script
native_nameNo, strongly recommended1-255The full name in the customer's native script. It is the strong basis for the name check, so send it whenever you hold it
emailYesa valid emailThe customer's email address with you. It may differ from their Coinland email

Two rules follow from the block being part of the contract:

  • Never send a placeholder. The identity you send is the identity that gets bound. A "guest" or "test" value binds a real Coinland account to the wrong identity, permanently -- see going live.
  • The identity is part of the idempotency fingerprint. Replaying the same reference_id with a different customer is PAY_DUPLICATE_REFERENCE (409), exactly like changing the amount would be.

The binding ceremony

The first time a given customer.id pays you, the hosted widget runs a binding ceremony before the payment can proceed:

The payer confirms the link, twice. Two explicit confirmations, each with an "I have read and understood" checkbox: that the Coinland account they are signed into is their own, and that it will be permanently linked to this customer identity at your business. There is no quiet consent -- a payer cannot bind an account without having read what that means.

Coinland checks the name. A fuzzy comparison between the name and native_name you supplied and the verified (KYC) name on the Coinland account. native_name is the strong basis for this check, which is why sending it is worth the extra column in your database.

The check decides what happens next.

  • Match -- the binding becomes active and the payment proceeds.
  • No match -- the binding becomes pending_review: the payment is refused, and the binding waits in your review queue. Approving it activates the binding without the customer re-running the ceremony; rejecting it retires the binding.

Completing any payment -- first or hundredth -- also requires the payer's two-factor (TOTP) code, the same security tier as a withdrawal. That step is entirely widget-side: the widget can surface TOTP_REQUIRED, TOTP_NOT_ENROLLED or TOTP_INVALID to the payer, and none of them reaches your integration.

After the binding

Once a binding is active:

  • Only the bound Coinland account can pay sessions created for that customer.id. Any other account is refused with PAY_BINDING_MISMATCH (403).
  • The customer can never change the binding. There is no customer-side unlink, re-link or transfer. Only you can revoke it.
  • One live binding per side. A customer.id that already holds a live binding cannot bind a second account, and a Coinland account bound to one of your customers cannot bind to another -- either attempt is PAY_BINDING_CONFLICT (409).

Everything below runs under your checkout key (Authorization: Bearer clpay_live_…), the same key that creates sessions.

Reading a binding

curl https://my.coinlandexchange.com/api/pay/v1/customers/user-1042/binding \
  -H "Authorization: Bearer $COINLAND_PAY_KEY"
200 OK
{
  "customer_id": "user-1042",
  "state": "active",
  "binding": {
    "binding_id": "5b8e2f40-7a13-4c96-8d2e-1f6a9c3b70e4",
    "customer_id": "user-1042",
    "payer_id": "7c1e5b90-3f42-4a86-9d05-2b8e4c1f6a37",
    "masked_name": "H**** K****",
    "supplied_name": "Hamid Khosravi",
    "supplied_name_native": "حمید خسروی",
    "supplied_email": "hamid@example.com",
    "status": "active",
    "match_score": "0.94000",
    "match_passed": true,
    "bound_at": "2026-08-12T10:15:22.000Z",
    "reviewed_at": null,
    "created_at": "2026-08-12T10:14:03.000Z"
  }
}

state is one of three values:

stateMeaningbinding
noneThis customer.id has never bound an accountnull
activeBound; the linked account can paythe binding object
pending_reviewThe name check failed; the customer cannot pay until you resolve itthe binding object

Two fields deserve a precise reading:

  • masked_name is the Coinland account's verified name, masked ("H**** K****"). The API never reveals the account's full name or its Coinland email -- the mask is enough to recognise the person you meant, and not enough to identify a stranger.
  • payer_id is the same opaque handle the payout API uses, so a binding hands you the recipient argument for a payout with no lookup.

supplied_name, supplied_name_native and supplied_email echo back what you sent on the session that created the binding -- your side of the comparison, next to match_score and match_passed, which record how the check went.

Reviewing a pending binding

A pending_review binding means a real person tried to pay and could not. It sits in your review queue in two places: the Customers tab of your business console, and the API below -- resolve it from whichever fits your operation, the result is the same.

Approve
curl -X POST https://my.coinlandexchange.com/api/pay/v1/customers/user-1042/binding/review \
  -H "Authorization: Bearer $COINLAND_PAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "approve"}'
Reject
curl -X POST https://my.coinlandexchange.com/api/pay/v1/customers/user-1042/binding/review \
  -H "Authorization: Bearer $COINLAND_PAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "reject", "reason": "Name does not match our records"}'

reason is required for reject and recorded against the binding. The response is the updated binding:

200 OK
{
  "binding": {
    "binding_id": "5b8e2f40-7a13-4c96-8d2e-1f6a9c3b70e4",
    "customer_id": "user-1042",
    "status": "active",
    "reviewed_at": "2026-08-12T11:02:47.000Z"
  }
}
  • Approve activates the binding. The customer does not re-run the ceremony -- their consent was already given; what was missing was your confirmation that the name discrepancy is fine. Their next payment attempt goes through.
  • Reject retires the binding. The next payment attempt by that customer starts a fresh ceremony, with whatever account they then sign in with.

Reviewing a binding that is not pending_review is PAY_BINDING_STATE (409): the action is only legal in that one state.

A pending review is a blocked customer, not a background task

While a binding sits in pending_review, that customer cannot pay you at all. Treat the binding.review webhook as a page to a human, not a row in a report -- the money arrives when someone decides.

Revoking a binding

Revocation is the statement "this Coinland account no longer belongs to this customer."

curl -X DELETE https://my.coinlandexchange.com/api/pay/v1/customers/user-1042/binding \
  -H "Authorization: Bearer $COINLAND_PAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Customer reports the account is no longer theirs"}'
200 OK
{
  "customer_id": "user-1042",
  "state": "revoked"
}

reason is required: severing an identity link is an auditable act, and the record of why belongs next to the record of what.

There is deliberately no way to name a replacement account in the same call. Revoking severs the link, nothing more; the next time that customer pays, a fresh ceremony runs and the new person proves themselves the same way the first one did -- consent, name check, and review if the check fails. An API that let you bind an arbitrary account directly would let a compromised key redirect a customer's payments, which is exactly what the ceremony exists to prevent.

Webhook events

Three events, delivered and signed exactly like every other event, and carrying the same caveat: the payload is a hint. Fetch GET /customers/{customer_id}/binding for authoritative state before acting.

TypeWhenWhat to do
binding.completedThe customer is bound -- the ceremony passed, or you approved a reviewMark the customer payable on your side
binding.reviewThe name check failed; the customer cannot pay until you approve or rejectAlert a human; resolve from the console or the review endpoint
binding.revokedThe link was severed -- a revoke or a rejectExpect a fresh ceremony on the customer's next payment
binding.review
{
  "event_id": "f83a2c16-4d95-4b70-8e21-6c0d9f3a5b48",
  "type": "binding.review",
  "binding_id": "5b8e2f40-7a13-4c96-8d2e-1f6a9c3b70e4",
  "customer_id": "user-1042",
  "status": "pending_review"
}

All three share this shape; status is active on binding.completed, pending_review on binding.review, and revoked on binding.revoked.

Errors

CodeHTTPMeaningWhat to do
PAY_CUSTOMER_REQUIRED422POST /sessions was called without the customer blockSend id, name and email on every create call. There is no opt-out
PAY_BINDING_REQUIRED403The payer tried to confirm before the ceremony bound themWidget-side; the payer completes the ceremony first. Nothing to fix in your code
PAY_BINDING_REVIEW403The binding is awaiting your review, so the payment is refusedApprove or reject it -- console Customers tab or the review endpoint
PAY_BINDING_MISMATCH403The signed-in Coinland account is not the one bound to this customer.idThe customer signs in with the bound account -- or, if the link is genuinely wrong, you revoke it
PAY_BINDING_CONFLICT409The customer.id or the Coinland account already holds a live bindingRead the binding and decide; revoke first if the existing link is the wrong one
PAY_BINDING_STATE409The action is illegal in the binding's current state, for example reviewing an active bindingRead the binding and branch on status

The confirm step can also answer TOTP_REQUIRED, TOTP_NOT_ENROLLED or TOTP_INVALID -- the two-factor step-up every payment carries. Those are surfaced to the payer inside the widget and never reach your integration.

"My customer changed their Coinland account"

The question every support desk eventually gets. The answer is one move: revoke the binding, with the reason, and do nothing else. The next time that customer pays, the widget runs a fresh ceremony against whatever account they sign in with, the name check runs again, and the new binding earns its active state the same way the old one did.

Do not try to "transfer" a binding by editing the customer record on your side: a new customer.id for the same person splits their history in your books and leaves the old id's binding live.

Checklist

  • customer sent on every create call, from your customer database, never a placeholder.
  • native_name included whenever you hold it -- it is what the name check leans on.
  • customer.id stable for the lifetime of the customer in your system.
  • binding.review alerts a human; someone owns the review queue.
  • Revocation is your one tool for account changes -- revoke and let the ceremony do the rest.

On this page