Skip to main content

Card Tokenization with Secure Fields

This page covers server-side card tokenization over the Stitch API. Encrypted inputs come from Stitch Secure Fields on the client. The response includes a reusable card.id for later charges.

Client-side integration

Before this server flow can run, your client app must:

  1. Install and render Secure Fields using the Secure Fields SDKs for your platform (Web, iOS, Android, Flutter, or React Native).
  2. Wait until the form reports isComplete and isValid.
  3. POST the encrypted payload (PAN/CVC tokens, expiry, bin, last four, cardholder name, and fingerprint) to your backend.
  4. Keep Stitch API credentials on the server only. The client never calls GraphQL with a secret.

The Client SDK owns the customer interaction and the encrypted payload. This page owns mapping that payload into the Stitch API and receiving card.id for later CIT/MIT charges.

tip

Full install, props, events, theming, and platform samples: Secure Fields SDKs.

Card tokenization with Secure Fields

Server-side Server tokenization: map Secure Fields inputs into initiateTransaction and receive a reusable card.id.

Inputs from the client

When the Secure Fields SDK reports a complete, valid payload, forward the encrypted card data and fingerprint to your backend. Your server maps those SDK fields into GraphQL initiateTransaction (or tokenization) inputs as follows:

ConceptWeb SDK (@stitch-money/react / web)Mobile typed APIs (iOS / Android / Flutter / React Native)GraphQL
Encrypted PANcard.numbercard.encryptedNumberpaymentMethods.card.cardDetails.redacted.redactedCardNumber (encryptedPan)
Encrypted CVCcard.cvccard.encryptedCvcpaymentMethods.card.cardDetails.redacted.redactedSecurityCode (encryptedSecurityCode)
Expiry month / yearcard.expiry.month / card.expiry.yearcard.expiryMonth / card.expiryYearexpiryMonth / expiryYear
BINcard.bincard.binbin
Last 4card.lastFourcard.lastFourlast4
Cardholder namecard.namecard.holderNamecardHolderName
Device fingerprintfingerprintfingerprintdeviceInformation.fingerprint
note

Always forward bin, last4, and fingerprint together with the encrypted PAN and CVC. The once-off and tokenization APIs require bin and last4; fingerprinting is required for many risk and consent flows.

Tokenize a Card

You can save a card in two ways, depending on who will initiate future charges:

If...Use this flow
The customer is present and will authenticate each future payment (e.g. "remember my card")Save for Future Customer-Initiated Transactions (CIT)
You need to charge the card later without the customer present (e.g. subscriptions, scheduled billing, etc.)Set Up Merchant-Initiated Transactions (MIT)

Customer-Initiated Transactions (CIT)

Use this flow to store a card for future payments where the customer is present and authenticates each charge. This is the right choice for "remember my card" checkout experiences.

Use the initiateTransaction mutation with storeOnFile: true to verify and tokenize the card (with or without a charge). The response will include a reusable card.id that you can store for future customer-initiated payments.

Associating a stored card with a payer

To associate the stored card with a payer, provide a non-empty, stable payerInformation.payerId in the same initiateTransaction request as paymentMethods.card.storeOnFile: true. After Stitch successfully verifies or charges the card, the returned card.id is associated with that payer ID for your client.

Both fields are required for the association. This applies to direct card tokenization using Secure Fields; Hosted UI consent requests use consent tokens and are not associated through this flow.

Authentication Scope

The initiateTransaction mutation requires a client token with the scope transaction_initiate. Use the GraphQL API URL https://api.stitch.money/graphql.

To verify a card, specify a zero amount within the initateTransaction mutation.

Merchant-Initiated Transactions (MIT)

Use this flow when you need to charge the card in the future without the customer present: for example, subscriptions, scheduled billing, or account top-ups.

To set up an MIT mandate, include recurringPayment in your initiateTransaction request with source: "payer". This records the customer's consent during this initial (customer-present) step. The agreement.reference must be unique and remain consistent across all future charges in the recurring series.

The response will include a reusable card.id that you can store for future customer-initiated payments.

Authentication Scope

The initiateTransaction mutation requires a client token with the scope transaction_initiate. Use the GraphQL API URL https://api.stitch.money/graphql.

To verify a card for an MIT mandate, specify a zero amount within the initateTransaction mutation.

API input fields
  • The nonce (4096-character limit) must be unique per request.
  • The externalReference (4096-character limit) can link the request to your order or payment reference.
  • Card details (encryptedPan, encryptedSecurityCode, etc.) come from the Secure Fields SDK.
  • For MIT mandates: recurringPayment.agreement.reference must be unique and consistent across the recurring series; use source: "payer" for this initial customer-initiated step.
  • The optional merchantId field routes the transaction to a pre-registered merchant on your client. See Specifying a Merchant below.

Specifying a Merchant

The optional merchantId field on the initiateTransaction input may be supplied to route the associated transaction charge to a specific pre-registered merchant on your client. A complete initiateTransaction example including merchantId is shown below:

When using a tokenized card via the token field for subsequent transactions, the merchant must be supplied independently on each transaction initiation where applicable. Neither merchantId nor externalMerchantId is retained per stored card token.

The merchantId field is an optional Stitch-supplied merchant identifier that routes the payment to a pre-registered merchant on your client. This is typically used in multi-merchant configurations (e.g. marketplaces or aggregator integrations), where funds for the payment should be associated with a specific sub-merchant or registered entity rather than the default merchant configured on your client.

When supplied, the value must reference a merchant that has been pre-provisioned by Stitch against your client. An example value is shown below:

bWVyY2hhbnQvNzhjMzc2NjUtZTgyNi00MGEyLWJiZDUtOTQ1YWNlYjYzMTc1
Provisioning Merchants

The merchantId value is a Stitch resource identifier (not your acquirer-assigned MID). Each pre-registered merchant may be configured for a different subset of payment methods. Requirements per merchant will be aligned with you during onboarding.

The optional externalMerchantId field is an alternative way to reference the same pre-registered merchant, using your own identifier instead of the Stitch resource identifier. This is useful where your systems already key merchants by an internal identifier, and you would prefer not to store the Stitch-supplied value alongside it.

The value is agreed with Stitch during merchant onboarding and recorded against the merchant's configuration, so you do not generate one per request. It is unique across the merchants registered on your client, between 1 and 64 characters long, and made up of letters, numbers, full stops, underscores and hyphens. An example value is shown below:

store-4471

At most one of merchantId or externalMerchantId may be supplied. Supplying both, or supplying an externalMerchantId that is not registered against your client, fails with a BAD_USER_INPUT error. The payment is never silently routed to the default merchant configured on your client.

Where the request also accepts a beneficiary, that field and the merchant are mutually exclusive. Settlement is directed to the beneficiary registered against the merchant, so supplying both fails with a BAD_USER_INPUT error.

Provide Payer information

The payerInformation object should be specified with information of the payer on your system's records. The set of provided information is used to increase the efficacy of fraud risk checks done by Stitch. All possible inputs may be found found in the API reference.

At a minimum, the payerId should always be specified within a request. This may be any internal identifier that always uniquely identifies users across Stitch requests.

Handle 3D Secure (3DS) Authentication

For flows that involve a transaction, the card issuer may require the customer to complete 3D Secure authentication. When this happens, the transaction is returned in a pending state with an interactionUrl. Redirect the customer to this URL to complete 3DS.

Rendering the 3DS Interaction

There are two ways to display the 3DS authentication flow to the user:

  1. Inside an iframe (recommended): This keeps the user on your checkout page and allows you to control the experience while safely handling errors or timeouts.
  2. As a full-page redirect: This navigates the user away from your site to the interaction URL and returns them via a redirect, but offers less control and no built-in recovery if something goes wrong.

Stitch recommends displaying the 3DS interaction inside an iframe, as this keeps the user within your checkout experience and provides safer recovery paths if the issuing bank’s challenge page fails to load or times out.

When using an iframe, the interaction url returned from the Stitch API must be loaded directly as the iframe’s src:

<iframe src="https://3ds.stitch.money/2ce31de7-a30d-4f3b-bfaa-cd06cb30db79"></iframe>

When the user finishes the 3DS flow, the interaction page running inside the iframe will notify your parent window using a postMessage event.

Listen for the message in your hosting page:

window.addEventListener('message', (event) => {
if (event.data?.type === 'finished') {
const { flow, externalReference, id: stitchId } = event.data;

// e.g. close modal, update UI, confirm transaction, etc.
}
});

The following fields are available on the message event:

FieldDescription
typefinished if the 3DS flow is complete
idThe Stitch ID of the transaction
flowThe 3DS interaction type of the transaction: challenge or frictionless
externalReferenceThe external reference of the transaction
statusThe final status of the transaction after the user's interaction: TransactionSuccess or TransactionFailure
statusReasonThe status reason, if applicable to the transaction status

We strongly recommend displaying the 3DS interaction within an overlay modal that sits on top of your checkout page. The modal should include a visible close or cancel button so that users always have a safe way to exit if the issuing bank’s challenge fails or times out. It should present the 3DS page inside its own isolated iframe container to keep the interaction clearly separated from the rest of your UI and it should offer a retry mechanism so users can attempt the authentication again if anything goes wrong. This approach ensures that customers are never left stuck or redirected to a broken page and gives you full control over the overall experience.

As an alternative, you may embed the iframe directly into the body of your page. If you choose this approach, you should still provide a clear retry or recovery option in case the challenge fails and you must ensure that users always have a safe and obvious way to exit the flow. This helps maintain a smooth checkout experience even without a modal.

2. Displaying the 3DS Flow as a full-page redirect

Alternatively, you can redirect the user to the 3DS interaction URL. In this flow, your checkout page is replaced entirely by the 3DS challenge screen and the user completes the authentication directly on that page. To initiate the flow, you simply take the interaction url returned by the Stitch API and perform a standard browser redirect to it (either by setting window.location.href in your frontend or by submitting a form to it).

To redirect the user back to your site once the 3DS flow is completed, you must supply a redirect_uri parameter during the initial redirect. For example, if you want the user returned to https://example.com/payment, you would append the following query string to the interaction URL: ?redirect_uri=https%3A%2F%2Fexample.com%2Fpayment.

The final URL you redirect the user to should look similar to:

https://3ds.stitch.money/2ce31de7-a30d-4f3b-bfaa-cd06cb30db79?redirect_uri=https%3A%2F%2Fexample.com%2Fpayment
danger

The URL specified as the redirect_uri must be secure i.e. an HTTPS URL.

Once the user has successfully completed the interaction and the payment has been processed, they will be redirected back to your specified redirect_uri with the following query parameters.

ParameterDescription
idThe Stitch ID of the transaction
flowThe 3DS interaction type of the transaction: challenge or frictionless
externalReferenceThe external reference of the transaction
statusThe final status of the transaction after the user's interaction: TransactionSuccess or TransactionFailure
statusReasonThe status reason, if applicable to the transaction status

Transaction Statuses

Flows that initiate a transaction can return the following states:

StatusDescription
TransactionPendingThe transaction has been initiated, but an interaction (e.g. 3DS) is required. An associated reason is returned.
TransactionSuccessThe transaction completed successfully. If 3DS was not required, this may be returned immediately. The card.id in the response is the stored token.
TransactionFailureThe transaction failed. An associated reason is returned.

Failure reasons

The TransactionFailure status indicates that the card transaction failed to be initiated, and includes a reason explaining the cause.

Potential failure reasons are detailed below:

ReasonDescription
authorizationFailedThe transaction was declined or blocked.
authorizationNotFinalisedThe transaction could not be processed by the acquirer.
blockedByFraudChecks

The transaction was blocked due to fraud checks. The reason for the block may be found in the reasonDescription field.

downstreamProviderErrorThe transaction could not be processed due to downstream error.
exceedsCardWithdrawalLimitThe transaction was declined due to withdrawal limits exceeded.
insufficientFundsThe transaction was declined due to insufficient funds.
internalServerErrorThe transaction could not be processed due to a server error.
invalidCardErrorThe transaction was declined due to an expired card.
invalidConfigurationErrorThe client has invalid or missing configuration.
invalidTransactionErrorThe transaction could not be processed due to invalid data.
secure3dDeclinedThe user has declined 3DS authentication.
secure3dLookupFailed3DS authentication attempt could not be initiated.
secure3dNotCompletedThe user has not completed 3DS authentication.
tokenDecryptionErrorThe payment token could not be decrypted.

Subscribe to Webhooks

Webhooks for transactions can be subscribed to by running the clientWebhookAdd mutation, to receive transaction webhook events. The transaction webhook will include details of the tokenized card in the card object.

If the subscription is successfully created, the body returned by the request will look similar to the sample in the Example Response tab in widget above.

For more information on receiving webhook events, listing active webhook subscriptions, unsubscribing from webhooks and validating signed webhook subscriptions, please visit the Webhooks page.

Webhook Statuses

The transaction webhook will be dispatched when a transaction reaches one of the following webhook status values:

  • SUCCESS
  • FAILURE
  • CANCELLED
note

The status field on the webhook payload uses these values (SUCCESS, FAILURE, CANCELLED, and — for preauthorized transactions awaiting capture — PENDING). These are distinct from the GraphQL TransactionState union types (TransactionSuccess, TransactionFailure, TransactionCancelled, TransactionPending) returned when querying a transaction over the API.

Example Payload
{
"data": {
"amount": {
"currency": "ZAR",
"quantity": "1"
},
"card": {
"bin": "41111111",
"cardHolderName": "Joe Soap",
"expiryMonth": 12,
"expiryYear": 2024,
"first6": "41111111",
"id": "Y2FyZC85YWY4OGE4MS05ZjNhLTRlNDItYWRiYy04ZTA1M2Q1YTM3M2U=",
"issuer": {
"name": "capitec",
"country": "ZA"
},
"last4": "1111",
"maskedPan": "411111******1111",
"network": "Visa",
"type": "Credit"
},
"createdAt": "2023-05-10T12:22:42.865Z",
"eci": "05",
"externalReference": "79261d16-c53b-48eb-9019-dc9cfb6c5126",
"feeAmount": null,
"id": "Y2FyZHRyYW5zYWN0aW9uLzQwNDMxRTY5LTNERjctNEIyQS1CNDY0LURFNTQwNDc0QkMxQw==",
"netAmount": null,
"nonce": "abb1c3b7-b39b-4a4c-93cb-3bbb363a3171",
"originalAmount": {
"currency": "ZAR",
"quantity": "1"
},
"paymentRequestId": "RRFyZHRyYW5zYWN0aW9uLzQwNDMxRTY5LTNERjctNEIyQS1CNDY0LURFNTQwNDc0QkMxQw==",
"primarySettlement": null,
"retrievalReferenceNumber": "508714102541",
"secure3dDecision": "skip",
"secure3dDecisionReason": "clientSpecified",
"splitSettlements": null,
"status": "SUCCESS",
"statusReason": null,
"reasonDescription": null,
"type": "CARD",
"updatedAt": "2023-05-10T12:22:42.865Z"
},
"datetime": "2023-05-10T12:22:42.865Z",
"id": "transaction:status:success:40431E69-3DF7-4B2A-B464-DE540474BC1C",
"type": "transaction"
}

Split settlements

primarySettlement and splitSettlements are null on an ordinary card transaction, as above. On a split payment they carry the allocation: primarySettlement.amount is the transaction amount less the sum of the splits, and each splitSettlements entry gives a destination merchant and its portion. The feeAmount and netAmount figures are exposed alongside the allocation — at the top level of data, on primarySettlement, and on each splitSettlements entry.

{
"feeAmount": { "currency": "ZAR", "quantity": "4.25" },
"netAmount": { "currency": "ZAR", "quantity": "395.75" },
"primarySettlement": {
"amount": { "currency": "ZAR", "quantity": "280.00" },
"feeAmount": { "currency": "ZAR", "quantity": "2.93" },
"netAmount": { "currency": "ZAR", "quantity": "277.07" }
},
"splitSettlements": [
{
"merchantId": "bWVyY2hhbnQvNzhjMzc2NjUtZTgyNi00MGEyLWJiZDUtOTQ1YWNlYjYzMTc1",
"amount": { "currency": "ZAR", "quantity": "120.00" },
"feeAmount": { "currency": "ZAR", "quantity": "1.32" },
"netAmount": { "currency": "ZAR", "quantity": "118.68" }
}
]
}

merchantId is the same merchant ID the API returns, so a webhook can be reconciled against a query result. feeAmount and netAmount are null until fees are computed (while the transaction is still pending), never zero.

Initiating a Transaction

List Stored Cards for a Payer

Use the Cards REST API to retrieve the active cards associated with a payer ID. This request requires a client token with the client_card scope.

curl -X GET 'https://api.stitch.money/v2/cards?payerId=payer-123&limit=20&offset=0' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
Query parameterRequiredDescription
payerIdYesThe stable, client-defined payer ID supplied when the card was stored.
limitNoThe maximum number of cards to return. Must be between 1 and 50; defaults to 20.
offsetNoThe number of cards to skip. Must be zero or greater; defaults to 0.

The response contains the card metadata you can use to display saved cards, together with pagination information:

{
"data": [
{
"id": "Y2FyZC82N2NlMTY5Mi1kMWQ3LTQ5NDAtYWVlNC1iMTE5MTk0M2NjYzQ=",
"bin": "42424242",
"last4": "4242",
"expiry": {
"month": "12",
"year": "29"
},
"network": "visa",
"fundingType": "debit",
"issuer": {
"name": "standard_bank",
"country": "ZA"
},
"createdAt": "2026-08-20T09:30:00Z",
"updatedAt": "2026-08-20T09:35:00Z"
}
],
"page": {
"limit": 20,
"offset": 0,
"hasNext": false
}
}

Only active cards associated with the payer ID for your authenticated client are returned. If the payer has no active associated cards, the API returns 200 OK with an empty data array and hasNext: false.

Initiate with a Stored Card

Use a returned card ID, such as card.id from initiateTransaction or data[].id from the Cards REST API, to initiate transactions for one-off or recurring payments.

Delete a Card Token

When a user unlinks a card from your platform, you should delete the stored token over the Stitch API so that transactions can no longer be initiated with that card.

Use the Cards REST API to delete the stored card. This request requires a client token with the client_card scope.

curl -X DELETE 'https://api.stitch.money/v2/cards/Y2FyZC82N2NlMTY5Mi1kMWQ3LTQ5NDAtYWVlNC1iMTE5MTk0M2NjYzQ=' \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Replace the path parameter with the card id returned by initiateTransaction or the Cards REST API. A successful deletion returns 204 No Content, and the card token can no longer be used to initiate transactions.

Test card numbers

Our sandbox environment allows simulating successful and failed card transactions, without requiring real debit or credit card details.

You may use the below card numbers on the card input screen to trigger the corresponding scenario.

tip

For all card numbers, any future date for the expiry date (in the format MM/YY), as well as any 3-digit value for the CVC, will process as expected.

ScenarioCard Number
3DS successful and payment completedAny valid card number, such as 4032035421088592
Transaction fails during 3DS verification4004462059871392
Transaction fails during payment authorization4032033425469975
Transaction fails due to insufficient funds4005519200000004
Transaction fails due to exceeding withdrawal limit5284989416854933
Transaction fails due to downstream provider error4787692003020026
Transaction initially has an unknown external state and completes successfully4000056655665556