Approve a Transfer, the legacy SMS PIN
This page documents the legacy fallback for clearing a transfer’s sender-approval stage: a numeric PIN sent by text and typed back. It still works, and it is being retired. If you are integrating now, use the passkey or device-approval path instead.
Where to go instead
A transfer is approved by the player proving who they are with a passkey (web and mobile) or a device approval grant (consoles, TVs, native Steam and desktop builds), and then your side calling POST /api/sdk/transfers/{id}/approve with that proof. That path is stronger, costs nothing to deliver, and is the one the platform is built around.
Why this is being retired
A code in a text message is bearer value: it authorises whoever reads the message first, and a number can be taken over. It is also the most expensive channel we have. Every other notification in these flows has already moved off it, the destination is told by webhook, guardian consent goes by email, and a new phone is vouched for by a code shown on the player’s own screen. The only message still sent as a matter of course is the claim link to a peer-send recipient we can reach no other way.
Do not build a new integration on this endpoint. No removal date is set; treat it as legacy and plan the passkey path.
The endpoint, for partners still on it
POST $BASE/api/transfers/verify-sms
X-Game-Secret-Key: <game secret>
Content-Type: application/json
{ "transaction_id": "TXN_1757000000_AB12CD", "sms_pin": "483920" }
200 OK
{
"status": "success",
"transaction_id": "TXN_...",
"new_balance": "950.00",
"claim_code": "KJMRS-47281",
"claim_instructions": {
"message": "...",
"target_game_id": "...", "target_game_name": "...",
"claim_code_expires_at": "2026-09-05T18:10:00+00:00"
},
"transfer_summary": { "amount_initiated": "50.00", "net_amount_for_claim": "45.00", "fees_deducted": "5.00",
"source_player_current_available_balance": "950.00" },
"order_id": "TFRO_..."
}It reaches exactly the same state as /api/sdk/transfers/{id}/approve: pending_claim, with the same claim code and the same transfer.claim_pending webhooks to both titles. The collecting side is unchanged either way, see Collecting, stage by stage.
| Answer | Means | Action |
|---|---|---|
400 invalid PIN | Wrong code. The body carries attempts_remaining. | Show the remaining attempts. The PIN locks out after three. |
400 expired PIN | The approval window closed. | Start a new transfer; the reservation is swept back to the sender. |
403 HIGH_VALUE_REQUIRES_APP | This amount is above the ceiling for the message-based path. | Approve with a passkey or device grant, the path you should be on anyway. |
403 PASSKEY_RECOVERY_COOLDOWN | Money out is held after a passkey recovery. | Show retry_after and stop. |
202 GUARDIAN_APPROVAL_PENDING | A minor account whose guardian has not answered. Not a failure. | Poll /api/transactions/{id}/approval-status, then retry. |
410 GUARDIAN_APPROVAL_REJECTED / _EXPIRED | Terminal. | Stop. |
429 | Too many verification attempts. Carries retry_after. | Back off for the stated time. |
Re-sending the PIN
POST $BASE/api/transfers/resend-pin
X-Game-Secret-Key: <game secret>
{ "transaction_id": "TXN_..." }
200 { "status": "resent" }
429 { "error": "resend_cooldown", "retry_after": 30 }
400 { "error": "pin_expired" }This exists only to support the legacy path, and it does nothing on a transaction whose sender is on the passkey or device-grant route, there is no PIN to resend there, which is rather the point.
If you are reading this to build something
You are on the wrong page. verification_method: "sms" on an initiate response tells you a fallback PIN was sent, it does not tell you to use it. The passkey and device-grant paths are live on that same transaction, and they are what your integration should call.