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:
- Install and render Secure Fields using the Secure Fields SDKs for your platform (Web, iOS, Android, Flutter, or React Native).
- Wait until the form reports
isCompleteandisValid. - POST the encrypted payload (PAN/CVC tokens, expiry,
bin, last four, cardholder name, andfingerprint) to your backend. - 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.
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:
| Concept | Web SDK (@stitch-money/react / web) | Mobile typed APIs (iOS / Android / Flutter / React Native) | GraphQL |
|---|---|---|---|
| Encrypted PAN | card.number | card.encryptedNumber | paymentMethods.card.cardDetails.redacted.redactedCardNumber (encryptedPan) |
| Encrypted CVC | card.cvc | card.encryptedCvc | paymentMethods.card.cardDetails.redacted.redactedSecurityCode (encryptedSecurityCode) |
| Expiry month / year | card.expiry.month / card.expiry.year | card.expiryMonth / card.expiryYear | expiryMonth / expiryYear |
| BIN | card.bin | card.bin | bin |
| Last 4 | card.lastFour | card.lastFour | last4 |
| Cardholder name | card.name | card.holderName | cardHolderName |
| Device fingerprint | fingerprint | fingerprint | deviceInformation.fingerprint |
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.
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.
The initiateTransaction mutation requires a client token with the scope transaction_initiate. Use the GraphQL API URL https://api.stitch.money/graphql.
- Verify Card
- Verify and Charge Card
To verify a card, specify a zero amount within the initateTransaction mutation.
To verify and charge a card, specify a non-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.
The initiateTransaction mutation requires a client token with the scope transaction_initiate. Use the GraphQL API URL https://api.stitch.money/graphql.
- Set Up MIT Mandate (No Initial Charge)
- Charge, Store and Set Up MIT Mandate
To verify a card for an MIT mandate, specify a zero amount within the initateTransaction mutation.
To verify and charge a card for an MIT mandate, specify a non-zero amount within the initateTransaction mutation.
- 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.referencemust be unique and consistent across the recurring series; usesource: "payer"for this initial customer-initiated step. - The optional
merchantIdfield 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
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:
- 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.
- 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.
1. Displaying the 3DS Flow Inside an iframe (Recommended)
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:
| Field | Description |
|---|---|
type | finished if the 3DS flow is complete |
id | The Stitch ID of the transaction |
flow | The 3DS interaction type of the transaction: challenge or frictionless |
externalReference | The external reference of the transaction |
status | The final status of the transaction after the user's interaction: TransactionSuccess or TransactionFailure |
statusReason | The 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
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.
| Parameter | Description |
|---|---|
id | The Stitch ID of the transaction |
flow | The 3DS interaction type of the transaction: challenge or frictionless |
externalReference | The external reference of the transaction |
status | The final status of the transaction after the user's interaction: TransactionSuccess or TransactionFailure |
statusReason | The status reason, if applicable to the transaction status |
Transaction Statuses
Flows that initiate a transaction can return the following states:
| Status | Description |
|---|---|
| TransactionPending | The transaction has been initiated, but an interaction (e.g. 3DS) is required. An associated reason is returned. |
| TransactionSuccess | The transaction completed successfully. If 3DS was not required, this may be returned immediately. The card.id in the response is the stored token. |
| TransactionFailure | The 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:
| Reason | Description |
|---|---|
| authorizationFailed | The transaction was declined or blocked. |
| authorizationNotFinalised | The 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 |
| downstreamProviderError | The transaction could not be processed due to downstream error. |
| exceedsCardWithdrawalLimit | The transaction was declined due to withdrawal limits exceeded. |
| insufficientFunds | The transaction was declined due to insufficient funds. |
| internalServerError | The transaction could not be processed due to a server error. |
| invalidCardError | The transaction was declined due to an expired card. |
| invalidConfigurationError | The client has invalid or missing configuration. |
| invalidTransactionError | The transaction could not be processed due to invalid data. |
| secure3dDeclined | The user has declined 3DS authentication. |
| secure3dLookupFailed | 3DS authentication attempt could not be initiated. |
| secure3dNotCompleted | The user has not completed 3DS authentication. |
| tokenDecryptionError | The 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:
SUCCESSFAILURECANCELLED
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 parameter | Required | Description |
|---|---|---|
payerId | Yes | The stable, client-defined payer ID supplied when the card was stored. |
limit | No | The maximum number of cards to return. Must be between 1 and 50; defaults to 20. |
offset | No | The 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.
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.
| Scenario | Card Number |
|---|---|
| 3DS successful and payment completed | Any valid card number, such as 4032035421088592 |
| Transaction fails during 3DS verification | 4004462059871392 |
| Transaction fails during payment authorization | 4032033425469975 |
| Transaction fails due to insufficient funds | 4005519200000004 |
| Transaction fails due to exceeding withdrawal limit | 5284989416854933 |
| Transaction fails due to downstream provider error | 4787692003020026 |
| Transaction initially has an unknown external state and completes successfully | 4000056655665556 |