Skip to content

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 ​

  1. Provision the cardholder with createPartnerCardUser. Store the cardUserId.
  2. Upload the proof of address, and the ID document if you hold it, with POST /partner/cards/kyc/documents.
  3. Start a session with createPartnerCardUserKycSession and kind: SDK_TOKEN. Load the widget with the token.
  4. Wait for card_user.kyc_status_changed. The webhook, not the widget's browser events, tells you the result.
  5. When nextStep is APPLY, call createCardApplicationForPartnerUser with the same cardUserId.

Agio sends Rain a share token from its own verified applicant. You do not send partnerKycShareToken on this path.

Before you start ​

  1. Collect agreement acceptance. The application in step 5 needs it.
  2. Send Agio the origins that host the widget, for example https://dev.partner.example and https://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.
  3. Subscribe to CARD_USER_KYC_STATUS_CHANGED, CARD_APPLICATION_STATUS_CHANGED and CARD_APPLICATION_COMPLETION_LINK_CHANGED. See receive webhooks.
  4. 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 fieldValue
cardUserIdThe provisioned cardUserId
idDocTypeUTILITY_BILL, PASSPORT, ID_CARD or DRIVERS
idDocSubTypeFRONT_SIDE or BACK_SIDE for ID_CARD and DRIVERS. Leave out for other types
countryISO 3166-1 alpha-2 or alpha-3 code of the document
fileNameOriginal file name, up to 255 characters
contentTypeimage/jpeg, image/png or application/pdf. An ID document takes JPEG or PNG only
fileBase64The 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.

HTTPerrorCodeAction
400INVALID_REQUESTFix the body, the country code, the base64 (empty or not canonical), or idDocSubType
400DOCUMENT_TYPE_NOT_ALLOWEDSend UTILITY_BILL, PASSPORT, ID_CARD or DRIVERS. The widget takes the selfie
401UNAUTHORIZEDThe request carries no valid partner credentials
403FORBIDDENThe cardUserId does not belong to your partner organization
409KYC_LOCKEDVerification is under review or approved. Do not upload again
413FILE_TOO_LARGECompress the file to 10 MB or less
415UNSUPPORTED_CONTENT_TYPESend a JPEG, PNG or PDF
422DOCUMENT_REJECTEDAsk for a clearer or newer document. errors can name the cause
429RATE_LIMITEDWait retryAfterSeconds, also sent in the Retry-After header
500INTERNAL_ERRORRetry. 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-Policy header, allow the camera and the microphone for your own origin and for https://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-Policy header, allow https://*.sumsub.com in frame-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): call createCardApplicationForPartnerUser.
  • AWAIT_REVIEW (PENDING_REVIEW): wait for the next event.
  • RESUBMIT (ACTION_REQUIRED): show reasonDescription, which is Sumsub's moderation comment. Call createPartnerCardUserKycSession again 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.

errorCodeMeaningAction
KYC_IN_PROGRESSVerification 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 onlyFinish the widget, then wait for the webhook. Do not send the hosted link to a white-label cardholder. Apply again after APPLY
KYC_NOT_APPROVEDVerification was rejected. kycStatus is REJECTED, with no linkRead partnerCardUserKycStatus. Follow the RESUBMIT or TERMINAL step
KYC_STATUS_UNAVAILABLEAgio could not read the verification statusRetry the call
KYC_SHARE_UNAVAILABLEAgio could not share the verified applicant with Rain right nowWait 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.

  1. Ask Agio to register your development origin for the widget.
  2. Expose your webhook receiver over a public HTTPS tunnel. Agio cannot reach localhost. Subscribe the tunnel URL to the three events above and save the signingSecret. The receiver must accept the subscription.created test delivery with a 200.
  3. Create a customer organization and a cardholder, and save the cardUserId.
  4. Upload a proof of address, and an ID document if you test that path. Expect success: true and an imageId for each.
  5. Request an SDK_TOKEN session, 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.
  6. Expect PENDING_REVIEW with AWAIT_REVIEW, then APPROVED with APPLY.
  7. Call createCardApplicationForPartnerUser with the agreement acceptance. Expect success: true.

Checks that must fail:

TestExpected result
Upload a SELFIE400 DOCUMENT_TYPE_NOT_ALLOWED
Upload an ID_CARD with no idDocSubType400 INVALID_REQUEST
Upload a file that is not a JPEG, PNG or PDF415 UNSUPPORTED_CONTENT_TYPE
Upload again after review started409 KYC_LOCKED
Apply before the APPLY webhookKYC_IN_PROGRESS with kycStatus
Apply for a rejected cardholderKYC_NOT_APPROVED, no link
Send a saved webhook body with one changed byteYour receiver returns 401
Send a saved webhook body with its original headers, laterYour receiver returns 401 for the old timestamp
Call resendPartnerWebhookDelivery for a handled eventYour receiver returns 200 and saves nothing new
Open the widget from an origin that Agio did not registerThe 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 from Rain.

Next: Create and manage cards.