Appearance
Upload documents you already hold
Use this path when you do not use Sumsub and you already hold your cardholder's documents. You upload the proof of address and, if you hold it, the ID document to Agio. The cardholder then takes the selfie and liveness check in a verification widget on your own page, and the ID document photo if you did not upload one. Agio verifies the cardholder, tells you by webhook, and you then submit the card application. The cardholder never repeats a document you already sent.
If you cannot embed the widget, use a hosted link as a fallback. If you verify cardholders in your own Sumsub workspace, send a share token.
How it works
- Provision the cardholder with
createPartnerCardUser. Store thecardUserId. - Upload the proof of address, and the ID document if you hold it, with
POST /partner/cards/kyc/documents. - Start a session with
createPartnerCardUserKycSessionandkind: SDK_TOKEN. Load the widget with the token. - Wait for
card_user.kyc_status_changed. The webhook, not the widget's browser events, tells you the result. - When
nextStepisAPPLY, callcreateCardApplicationForPartnerUserwith the samecardUserId.
Agio sends Rain a share token from its own verified applicant. You do not send partnerKycShareToken on this path.
Before you start
- Collect agreement acceptance. The application in step 5 needs it.
- Send Agio the origins that host the widget, for example
https://dev.partner.exampleandhttps://app.partner.example: scheme, host and port, no path. Agio adds them to the verification provider's allowlist, and the widget does not load on any other origin. Register development and production origins separately, and allow time for the change before you test. - Subscribe to
CARD_USER_KYC_STATUS_CHANGED,CARD_APPLICATION_STATUS_CHANGEDandCARD_APPLICATION_COMPLETION_LINK_CHANGED. See receive webhooks. - Serve your verification page over HTTPS, and allow the camera.
Upload the documents
POST /partner/cards/kyc/documents takes one document per request, or one side of a two-sided ID document. It takes UTILITY_BILL for the proof of address, and PASSPORT, ID_CARD or DRIVERS for the ID document. Send no selfie here: the widget captures the selfie and liveness check live, and compares the face with the ID document.
For ID_CARD and DRIVERS, send each side in its own request with idDocSubType set to FRONT_SIDE or BACK_SIDE. Send both sides: with one side only, the ID document step does not complete. For PASSPORT and UTILITY_BILL, leave out idDocSubType.
The route is on the same host as the GraphQL endpoint (https://dev.api.agiodigital.com in development), with the same API key and HMAC signature. See authentication. Sign the exact body that you send. Set the body to JSON with these fields and no others. An extra field returns INVALID_REQUEST.
| Body field | Value |
|---|---|
cardUserId | The provisioned cardUserId |
idDocType | UTILITY_BILL, PASSPORT, ID_CARD or DRIVERS |
idDocSubType | FRONT_SIDE or BACK_SIDE for ID_CARD and DRIVERS. Leave out for other types |
country | ISO 3166-1 alpha-2 or alpha-3 code of the document |
fileName | Original file name, up to 255 characters |
contentType | image/jpeg, image/png or application/pdf. An ID document takes JPEG or PNG only |
fileBase64 | The file as canonical base64 (with padding, no line breaks). Up to 10 MB decoded. |
Agio holds the file in memory for the request and stores nothing. The first upload creates the cardholder's verification applicant from the identity you provisioned.
python
import base64, hashlib, hmac, json, os, time
import httpx
ORIGIN = os.environ.get("AGIO_PARTNER_ORIGIN", "https://dev.api.agiodigital.com")
body = json.dumps({
"cardUserId": card_user_id,
"idDocType": "UTILITY_BILL",
"country": "NLD",
"fileName": "bill.pdf",
"contentType": "application/pdf",
"fileBase64": base64.b64encode(data).decode(),
}).encode()
timestamp = str(int(time.time() * 1000))
signature = hmac.new(os.environ["AGIO_PARTNER_CLIENT_SECRET"].encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
response = httpx.post(
f"{ORIGIN}/partner/cards/kyc/documents",
content=body,
timeout=60,
headers={
"content-type": "application/json",
"x-agio-api-key": os.environ["AGIO_PARTNER_API_TOKEN"],
"x-agio-timestamp": timestamp,
"x-agio-signature": signature,
},
)Set AGIO_PARTNER_ORIGIN to the production host when you use production credentials.
Response
json
{ "success": true, "imageId": "<document image ID>", "errors": [], "warnings": [] }A failure has success: false, an errorCode and an errorMessage. The HTTP status carries the class of failure.
| HTTP | errorCode | Action |
|---|---|---|
| 400 | INVALID_REQUEST | Fix the body, the country code, the base64 (empty or not canonical), or idDocSubType |
| 400 | DOCUMENT_TYPE_NOT_ALLOWED | Send UTILITY_BILL, PASSPORT, ID_CARD or DRIVERS. The widget takes the selfie |
| 401 | UNAUTHORIZED | The request carries no valid partner credentials |
| 403 | FORBIDDEN | The cardUserId does not belong to your partner organization |
| 409 | KYC_LOCKED | Verification is under review or approved. Do not upload again |
| 413 | FILE_TOO_LARGE | Compress the file to 10 MB or less |
| 415 | UNSUPPORTED_CONTENT_TYPE | Send a JPEG, PNG or PDF |
| 422 | DOCUMENT_REJECTED | Ask for a clearer or newer document. errors can name the cause |
| 429 | RATE_LIMITED | Wait retryAfterSeconds, also sent in the Retry-After header |
| 500 | INTERNAL_ERROR | Retry. A verification-provider failure that is not a bad document also lands here |
Limits
Each partner organization can send 30 uploads a minute, and 10 a minute for one cardholder. A file is at most 10 MB.
Start the verification session
Call createPartnerCardUserKycSession(cardUserId, kind) from your server and return token (SDK_TOKEN) or url (HOSTED_LINK) in the result, with success, errorCode and errorMessage. The SDK_TOKEN is short-lived: give it only to the signed-in cardholder it belongs to, and never log it. The widget asks your server for a fresh token before the first expires, so keep that route cheap. Call again for a new token.
The call returns KYC_ALREADY_APPROVED when verification is approved, and KYC_REJECTED when it was rejected.
kind: HOSTED_LINK opens a verification page that is not branded as yours, so use it only as a fallback. Do not load it in an iframe.
Load @sumsub/websdk in the browser with the token. The second argument to init fetches a new token when the first is about to expire. Stop the widget on unmount, so React Strict Mode does not mount two. The browser events only update your screen. Approval comes from the webhook.
To read the state at any time, query partnerCardUserKycStatus(cardUserId). It is read-only and returns status, nextStep, reason and steps { step status reason }. status is NOT_STARTED, IN_PROGRESS, PENDING_REVIEW, ACTION_REQUIRED, APPROVED or REJECTED. nextStep names the first step that still needs the cardholder, or is null.
Allow the camera
- Serve the page over HTTPS. Browsers block the camera on plain HTTP.
- If your site sends a
Permissions-Policyheader, allow the camera and the microphone for your own origin and forhttps://api.sumsub.com. A header that blocks the camera stops the selfie step with no error on your side. - If your page runs inside another iframe, that outer iframe needs
allow="camera; microphone; fullscreen". - If your site sends a
Content-Security-Policyheader, allowhttps://*.sumsub.cominframe-src.
These origins come from the widget provider. Check Sumsub's current origin list before you go live.
Receive the status webhook
Agio sends card_user.kyc_status_changed when the cardholder's verification is submitted, approved or rejected. The payload and the full status and nextStep table are in Cardholder KYC status changed. Act on nextStep:
APPLY(APPROVED): callcreateCardApplicationForPartnerUser.AWAIT_REVIEW(PENDING_REVIEW): wait for the next event.RESUBMIT(ACTION_REQUIRED): showreasonDescription, which is Sumsub's moderation comment. CallcreatePartnerCardUserKycSessionagain so the cardholder can correct it, and upload again if needed.TERMINAL(REJECTED): final. Stop, and contact Agio if the decision needs review.
NOT_STARTED and IN_PROGRESS send no event. Read them with partnerCardUserKycStatus. Verify each delivery on the raw body before you parse it. Use eventId to ignore a delivery you already handled: a retry keeps the same eventId.
Submit the application
When the webhook says APPLY, call createCardApplicationForPartnerUser as in submit an application. Send the cardUserId, the agreement acceptance and no partnerKycShareToken. Once the cardholder is approved, Agio sends Rain a share token from the verified applicant.
If you call it before Agio approves the cardholder, the response is not a Rain decision. It has success: false and the verification state. No application is created, and nothing is sent to Rain.
errorCode | Meaning | Action |
|---|---|---|
KYC_IN_PROGRESS | Verification is not approved yet. kycStatus is NOT_STARTED, IN_PROGRESS, ACTION_REQUIRED or PENDING_REVIEW. kycVerificationUrl is a fresh hosted link for NOT_STARTED, IN_PROGRESS and ACTION_REQUIRED only | Finish the widget, then wait for the webhook. Do not send the hosted link to a white-label cardholder. Apply again after APPLY |
KYC_NOT_APPROVED | Verification was rejected. kycStatus is REJECTED, with no link | Read partnerCardUserKycStatus. Follow the RESUBMIT or TERMINAL step |
KYC_STATUS_UNAVAILABLE | Agio could not read the verification status | Retry the call |
KYC_SHARE_UNAVAILABLE | Agio could not share the verified applicant with Rain right now | Wait and call again. Contact Agio if it continues |
When Rain asks for more
Agio approving the cardholder does not decide the application. Rain still reviews it and can ask for more, which moves the application to NEEDSVERIFICATION or NEEDSINFORMATION. You receive card_application.status_changed with the status and a completionUrl. Rain can issue a new link while the status stays the same: you then receive card_application.completion_link_changed (opt-in) with the latest completionUrl. Send the cardholder to the latest link by the hosted link steps. The page is Rain's, not yours. Wait for card_application.status_changed: no resubmit call is needed. Replace the stored link with the newest one, and clear it when the status leaves these states.
Test in development
Use the development endpoint and Agio development credentials, never production ones. Development uses the issuer's sandbox, so no real person goes through verification.
- Ask Agio to register your development origin for the widget.
- Expose your webhook receiver over a public HTTPS tunnel. Agio cannot reach
localhost. Subscribe the tunnel URL to the three events above and save thesigningSecret. The receiver must accept thesubscription.createdtest delivery with a200. - Create a customer organization and a cardholder, and save the
cardUserId. - Upload a proof of address, and an ID document if you test that path. Expect
success: trueand animageIdfor each. - Request an
SDK_TOKENsession, open your page from your development origin, and complete the selfie and liveness steps with the sandbox test images. The widget also asks for the ID document if you did not upload one. - Expect
PENDING_REVIEWwithAWAIT_REVIEW, thenAPPROVEDwithAPPLY. - Call
createCardApplicationForPartnerUserwith the agreement acceptance. Expectsuccess: true.
Checks that must fail:
| Test | Expected result |
|---|---|
Upload a SELFIE | 400 DOCUMENT_TYPE_NOT_ALLOWED |
Upload an ID_CARD with no idDocSubType | 400 INVALID_REQUEST |
| Upload a file that is not a JPEG, PNG or PDF | 415 UNSUPPORTED_CONTENT_TYPE |
| Upload again after review started | 409 KYC_LOCKED |
Apply before the APPLY webhook | KYC_IN_PROGRESS with kycStatus |
| Apply for a rejected cardholder | KYC_NOT_APPROVED, no link |
| Send a saved webhook body with one changed byte | Your receiver returns 401 |
| Send a saved webhook body with its original headers, later | Your receiver returns 401 for the old timestamp |
Call resendPartnerWebhookDelivery for a handled event | Your receiver returns 200 and saves nothing new |
| Open the widget from an origin that Agio did not register | The widget does not load |
White label
The cardholder sees only what you show them:
- The widget runs on your page, on your registered origin. Use
SDK_TOKEN. The hosted link opens a page that is not yours. - Agio and the verification provider send the cardholder no email and no text during verification.
- Agio sends no card notification to a partner cardholder.
- Rain's hosted page appears only when Rain asks for more, as described above.
- Rain sends the 3DS code for online card purchases by SMS from the sender
Rain. You cannot change it yet. Agio tracks forwarding the code to you in #5281. Until it ships, tell cardholders to expect a text fromRain.
Next: Create and manage cards.