Appearance
Onboard a cardholder
Create a customer organization, provision the cardholder's identity, collect agreement acceptance, and submit an application. Issue a card after the application is approved.
Before you start
Every application needs agreement acceptance. Send agreementsAcceptance with the current agreementVersion, which is 2026-08-24.1 at the time of writing. Without agreementsAcceptance, the application returns success: false with errorCode: "ATTESTATION_REQUIRED". A version that is no longer accepted returns a validation error that names the current version. Read the version from the changelog and do not copy an old one from a past request. See collect agreement acceptance.
Provision a customer organization
The customer organization groups cardholders you manage. It is separate from your partner organization, which the API derives from your key.
A customer organization is one real client, such as a company. That client has many cardholders, and all of them go under the same organization. For a consumer program without a business customer, use one organization for the program.
Do not create an organization for each cardholder. Do not create one for each application attempt either. Both are wrong: each extra organization stays empty or splits one client's cardholders across many records.
graphql
mutation CreateCustomer($input: CreatePartnerCustomerOrganizationInput!) {
createPartnerCustomerOrganization(input: $input) {
success
organizationId
error
}
}json
{ "input": { "name": "Acme Corp" } }Store organizationId after success: true. This operation has no idempotency key; do not repeat it blindly if the response is lost. A customer organization is reusable: keep it and provision later cardholders or retries under it. Do not create a new organization for each attempt.
To onboard a client, create its organization once, store the organizationId, and then create each cardholder under it. This sketch uses the partnerQuery helper from the quickstart, the CreateCustomer and CreateCardholder operations in this guide, and an identity list collected by your onboarding flow:
ts
const customer = (await partnerQuery(CREATE_CUSTOMER, { input: { name: "Acme Corp" } })).createPartnerCustomerOrganization;
if (!customer.success) throw new Error(customer.error);
await saveOrganization("acme", customer.organizationId);
for (const identity of cardholderIdentities) {
const data = await partnerQuery(CREATE_CARDHOLDER, {
input: { ...identity, customerOrganizationId: customer.organizationId }
});
const result = data.createPartnerCardUser;
if (!result.success) throw new Error(`${result.errorCode}: ${result.errorMessage}`);
await saveCardholder(identity.email, result.cardUserId, result.customerOrganizationId);
}Run the first call once for the client. Later cardholders of the same client skip it and read the stored organizationId.
Provision a cardholder
Send the identity and contact details collected through your onboarding flow. addressCountryCode is an ISO 3166 alpha-2 code; phoneCountryCode is the dialling code and phoneNumber the national number, and the two must form a valid number together — spacing/punctuation, a stray leading + or 00 prefix, a duplicated country code, and a domestic trunk 0 on either field are all normalized automatically, so send any common format; birthDate uses YYYY-MM-DD. From 2026-10-09 an invalid phone pair that stays invalid after normalization returns a warning instead of blocking the call; from 2026-10-26 it returns VALIDATION_ERROR — fix it before then by sending phoneNumber as the national number only (no country code, no leading 0/+) and phoneCountryCode as digits only.
graphql
mutation CreateCardholder($input: CreatePartnerCardUserInput!) {
createPartnerCardUser(input: $input) {
success
cardUserId
reused
customerOrganizationId
errorCode
errorMessage
}
}json
{
"input": {
"customerOrganizationId": "<organizationId>",
"firstName": "Alice",
"lastName": "Tester",
"email": "[email protected]",
"addressLine1": "1 Main St",
"addressCity": "Brooklyn",
"addressRegion": "NY",
"addressPostalCode": "11201",
"addressCountryCode": "US",
"phoneCountryCode": "1",
"phoneNumber": "2025551234",
"birthDate": "1990-01-15",
"nationalId": "<cardholder national identifier>"
}
}Store the returned cardUserId for the application. Provisioning is idempotent on the email, compared without case or surrounding spaces. Within the organization you send, the email alone returns the existing cardholder. Under another organization your key manages, the last name and birth date must also match. On a match the call returns the existing cardUserId with reused: true, and customerOrganizationId names the organization that holds it, which can differ from the one you sent. Use the returned values; an organization you created for the attempt stays empty. Repeating the call does not update the stored profile.
All identity fields above are required. Blank fields, an invalid birth date, or an invalid country code return success: false with errorCode: "VALIDATION_ERROR" and details in errorMessage; an invalid phone pair does too once the launch period above ends. Agio encrypts nationalId at rest and decrypts it when forwarding identity to the card processor. For a US cardholder, nationalId is the 9-digit Social Security Number; send the digits only.
Correct details before applying
patchPartnerCardUser changes supplied identity fields and keeps omitted fields. It accepts the provisioning fields except email and customerOrganizationId. The email is the provisioning idempotency key; the organization comes from the existing record.
graphql
mutation CorrectCardholder($input: PatchPartnerCardUserInput!) {
patchPartnerCardUser(input: $input) {
success
errorCode
errorMessage
}
}json
{
"input": {
"cardUserId": "<provisioned cardUserId>",
"addressLine1": "2 Main St",
"addressCity": "Brooklyn",
"addressRegion": "NY",
"addressPostalCode": "11201",
"addressCountryCode": "US"
}
}The merged address must remain complete. After an application exists for this cardholder, the mutation returns APPLICATION_ALREADY_SUBMITTED. Use this correction step before submitting, including when an application attempt returns INCOMPLETE_ADDRESS.
Collect agreement acceptance
Present the applicable card agreements to the cardholder before submitting their application. Record what they accepted, its version and hashes, and when they accepted it. agreementsAcceptance is required; isTermsOfServiceAccepted: true is also required and does not replace the evidence.
Every application needs this evidence. An application without agreementsAcceptance returns success: false with errorCode: "ATTESTATION_REQUIRED". An agreementVersion that is no longer accepted returns a validation error that names the current version and the versions still accepted. The partner API changelog announces each new agreement set.
Use the agreement set Agio supplies for your configured card program and jurisdiction. Confirm the applicable document keys during integration setup; the customer organization used to scope API records does not determine the legal agreement set.
| Document key | Raw agreement |
|---|---|
card-terms-us | US card terms |
account-opening-privacy-us | US account-opening privacy notice |
card-terms-international | International card terms |
card-terms-us-b2b | US business card terms |
card-terms-international-b2b | International business card terms |
authorized-user-agreement | Authorized user agreement |
Fetch each applicable raw Markdown document and hash the response bytes with SHA-256. Do not normalize whitespace or re-encode the text before hashing. Present that same text to the cardholder and send its lowercase hex hash. Confirm the current agreement-set version with Agio when configuring your integration.
bash
curl -fsS https://www.agiodigital.com/agio-digital-ltd-card-terms-us-program.md -o card-terms-us.md
shasum -a 256 card-terms-us.md| Acceptance field | Value |
|---|---|
agreementVersion | Published agreement-set version |
documentHashes | One { key, sha256 } for each applicable document |
acceptedAt | ISO 8601 timestamp from your acceptance record |
endUserIpAddress | Optional cardholder IP when accepting |
endUserAgent | Optional cardholder browser or application user agent when accepting |
The end-user IP is the cardholder's address at acceptance, not your API server's IP. acceptedAt is your attestation; Agio records a separate server receipt time and retains consent evidence for five years. Missing evidence returns ATTESTATION_REQUIRED; invalid evidence returns a validation error. Correct the evidence before retrying.
Submit an application
Choose exactly one collateral-admin option:
createWallet: trueuses your configured partner treasury as the collateral admin for this cardholder. It does not create a separate personal wallet. Your treasury must be configured by Agio first.walletAddresssupplies an EVM address whose owner will administer the collateral. Partner treasury withdrawal cannot sign for a customer's self-custody address.
graphql
mutation Apply($input: CreatePartnerCardApplicationInput!) {
createCardApplicationForPartnerUser(input: $input) {
success
applicantId
cardApplicationId
cardApplicationExternalId
applicationStatus
applicationCompletionUrl
walletAddress
errorCode
error
}
}json
{
"input": {
"cardUserId": "<provisioned cardUserId>",
"createWallet": true,
"occupation": "Software Developers",
"annualSalary": "100000",
"accountPurpose": "Business expenses",
"expectedMonthlyVolume": "5000",
"isTermsOfServiceAccepted": true,
"agreementsAcceptance": {
"agreementVersion": "<published agreement-set version>",
"documentHashes": [
{ "key": "card-terms-us", "sha256": "<SHA-256 of presented US terms>" },
{ "key": "account-opening-privacy-us", "sha256": "<SHA-256 of presented privacy notice>" }
],
"acceptedAt": "2026-10-04T06:00:00.000Z",
"endUserIpAddress": "203.0.113.7",
"endUserAgent": "PartnerApp/2.4"
}
}
}The agreement keys above illustrate a US document set; replace them with the keys and hashes Agio supplies for your program. occupation accepts a supported SOC code or description; see occupation codes.
Choose a verification path
| Your setup | Guide |
|---|---|
| You verify cardholders, with a selfie, in your own Sumsub workspace | Verify with your Sumsub workspace |
| You do not use Sumsub, or you cannot collect a selfie | Verify with a hosted link |
| You do not use Sumsub, and you already hold the cardholder's documents | Upload documents you already hold |
With a share token, Agio uses the token in the KYC handover and still forwards the provisioned identity. Without a share token, Agio uses the stored identity, and the cardholder completes any missing step on a hosted page.
Identifiers
| Identifier | Use |
|---|---|
Provisioned cardUserId / response applicantId | Submit the application and identify your provisioned applicant |
cardApplicationId | Numeric AgioCard_card_application.id; query the application and pass it to createCard |
Application applicant_id | Your provisioned cardUserId; filter applications by it |
cardApplicationExternalId | The issuer's cardholder UUID, stored as card_application_external_id |
Application, card and transaction card_user_id | The issuer's cardholder UUID, not your provisioned cardUserId; use it for balance, funding and withdrawal calls |
Your provisioned cardUserId identifies the applicant and does not change. The issuer assigns its own cardholder UUID when it accepts the application. Agio stores that UUID as card_user_id on the application, its cards and its transactions, under a second cardholder record. Your provisioned cardholder record does not carry the application status. Read status, the issuer UUID and cards from the application, found by applicant_id. Partner-issued cards have no Agio user_id; their customer organization and card company provide the ownership scope.
Check application status
Query by the numeric ID returned on application creation, or by applicant_id with your provisioned cardUserId. Its card_user_id is the issuer's cardholder UUID.
graphql
query ApplicationStatus($applicationId: Int!) {
AgioCard_card_application(where: { id: { _eq: $applicationId } }, limit: 1) {
id
applicant_id
card_application_external_id
card_user_id
application_status
application_completion_link
deposit_address
deposit_chain_id
updated_at
}
}json
{ "applicationId": 42 }Poll at a modest interval, such as once a minute, or use application webhooks. This is client guidance, not a guaranteed per-application polling limit.
| Status | Action |
|---|---|
PENDING | Wait for verification and provisioning |
NEEDSINFORMATION, NEEDSVERIFICATION | Give the signed completion link to the cardholder |
NOTSTARTED, TOSNOTACCEPTED | Give the signed completion link to the cardholder |
MANUALREVIEW | Wait for compliance review |
APPROVED, ACTIVE | Create a card when issuer cardholder data is ready |
DENIED, CANCELED | Stop issuance; contact Agio if the decision needs review |
For NEEDSINFORMATION, NEEDSVERIFICATION, NOTSTARTED, and TOSNOTACCEPTED, completionUrl is the link to give the cardholder and nextStep is AWAIT_REVIEW. NOTSTARTED means the cardholder has not begun the application. TOSNOTACCEPTED means the cardholder has not accepted the terms of service. After the cardholder finishes, watch for card_application.status_changed. No resubmit call is needed. For declines, treat nextStep: TERMINAL as final and RESUBMIT as retryable.
applicationCompletionUrl from the mutation and webhook completionUrl are composed signed links: open them as returned. In application_completion_link, prefer completionUrl when present. Older records may contain url with a params object; append every parameter to the URL. If there are no parameters, use url. Stripping the signature or rebuilding a link from a cardholder ID prevents verification.
If the application request times out before you receive an ID, query AgioCard_card_application with where: { applicant_id: { _eq: "<provisioned cardUserId>" } } before retrying. Application creation does not supply an idempotency key. While the cardholder has an application that is still in progress (not APPROVED, ACTIVE, DENIED, CANCELED, REJECTED, CLOSED or MIGRATED_DEPRECATED), a new submission returns success: false with errorCode: "APPLICATION_EXISTS", and cardApplicationId and applicationStatus describe the existing application. Track that application instead of submitting again, and query it for application_completion_link when it needs the cardholder.
Always apply with the provisioned cardUserId from createPartnerCardUser. After approval, the cardholder also has an issuer cardholder ID (card_user_id on the application). Do not use it as cardUserId. A submission with that ID returns success: false with errorCode: "USE_PROVISIONED_ID", and applicantId holds the provisioned cardUserId to use instead.
A cardholder needs only one approved application. When the cardholder already has an application with status APPROVED or ACTIVE, a new submission returns success: false with errorCode: "ALREADY_ACTIVE". cardApplicationId and applicationStatus describe the existing application, and cardApplicationExternalId holds its issuer cardholder ID. Issue more cards on that application with createCard. After a DENIED application, you can still resubmit for the same cardholder.
Next: Create and manage cards.