Integrating Once-off Card with Secure Fields
This page covers server-side once-off Card payments over the Stitch API. Encrypted card inputs are captured on the client with Stitch Secure Fields, then passed into initiateTransaction from your backend.
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 handling 3DS, webhooks, and status.
Full install, props, events, theming, and platform samples: Secure Fields SDKs.
Once-off Card with Secure Fields
Server-side Server initiation for once-off Card: map Secure Fields inputs into initiateTransaction, then handle 3DS, webhooks, and status.
For any transactions that require 3DS, you receive an interactionUrl to redirect the user. On successful initiation without pending interaction, the user's card has been charged.
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.
Initiating a Transaction
On receiving the corresponding card details from the SDK, these should be passed back to Stitch within the initiateTransaction API call. This requires a client token with the scope transaction_initiate.
The GraphQL API URL https://api.stitch.money/graphql can be used for all transaction initiation requests (for both test and live clients).
An example query is shown below:
- The
externalReferencefield (4096-character limit) field may be used to associate the transaction to your payment/order's reference. - The
noncefield (4096-character limit) is required to be unique per request, and hence, transaction. - The
encryptedPanandencryptedSecurityCode(and related card metadata) are supplied by your client via the Secure Fields client-side integration. Map them using the field mapping above.
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.
3DS-Enforcement
Typically, 3DS-enforcement rules are automatically determined by Stitch for all requests processed on your client, as described here.
However, if there is a requirement to specify 3DS-enforcement on a per-request basis, the paymentMethods.card.requireSecure3d parameter can be used to enable or disable 3DS enforcement. Contact the technical team to assist in enabling your client for this feature.
Supplying Your Own 3DS Results
If you perform 3DS authentication externally (outside of Stitch), you can supply your authentication results directly via the paymentMethods.card.secure3d input object. When provided, Stitch will not initiate any 3DS, but rather process the transaction with your given authentication data.
The secure3d object requires three fields:
| Field | Type | Description |
|---|---|---|
eci | String (required) | The Electronic Commerce Indicator from your 3DS authentication. Scheme-specific values apply (Visa: 05/06/07, Mastercard: 00/01/02/04). |
cryptogram | String (required) | The CAVV (Cardholder Authentication Verification Value), a Base64-encoded cryptographic proof that 3DS authentication was performed. |
dsTransactionId | String (required) | The Directory Server Transaction ID from the 3DS 2.x authentication flow. |
Example values (to be supplied within a transaction initiation):
{
"paymentMethods": {
"card": {
"secure3d": {
"eci": "05",
"cryptogram": "AJkBBkhgQRhgRTGHCQADCAAAAAA=",
"dsTransactionId": "f25084f0-5b16-4c0a-ae5d-b24e5e631c1a"
}
}
}
}
The secure3d object cannot be combined with requireSecure3d: true. Supplying both will result in a validation error, since requireSecure3d: true instructs Stitch to perform 3DS internally, which conflicts with externally supplied 3DS results.
The following combinations are valid:
requireSecure3d | secure3d present | Behavior |
|---|---|---|
true | No | Stitch performs 3DS internally (existing flow) |
true | Yes | Validation error |
false | No | 3DS is skipped entirely (existing flow) |
false | Yes | Stitch bypasses internal 3DS and forwards your data to the processor |
| unset | No | Stitch performs 3DS internally (existing flow) |
| unset | Yes | Stitch bypasses internal 3DS and forwards your data to the processor |
Metadata
The metadata field may be populated with any additional metadata relating to your payment request, such as further order information, shipping information, or sub-entity for which the payment is being processed.
This must be specified as an object of type Json, that includes key-value pairs. Note that nested objects must be JSON-stringified.
Recommended Metadata for Fraud Checks
To assist with fraud and risk checks performed on payments for physical goods, include shipping information within your metadata, as shown in the request examples. Values to be specified include:
billingAddressof the user,shippingAddressof the order,deliveryMethodnoting whether this is adeliveryorcollectionorder, andgiftCardAmountif a gift card or discount code was used in conjunction with the order's payment.
Specifying a Merchant
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.
Handling Transaction Responses
After initiating a transaction, one of two scenarios typically occur:
- If 3DS is not required, transaction initiation is immediately successful. The returned status in the response will indicate this, and a corresponding webhook will be sent.
- Alternatively, if 3DS verification is required by the user, the transaction status will be pending, and you will need to redirect the user to complete this step (as described below).
In the scenario of 3DS verification being required, an interactionUrl is returned as part of the response.
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 |
You will also receive a corresponding webhook to confirm the status of the transaction.
Transaction Statuses
The table below describes the different statuses a transaction can have:
| Status | Description |
|---|---|
| TransactionPending | The transaction has been initiated, but an interaction is required for the transaction to be successfully completed. An associated reason will be returned with this status. |
| TransactionSuccess | The transaction has been successfully completed, and user's card has been charged. If 3DS is disabled, or not required for the transaction, this will be returned immediately on successful initiation. |
| TransactionFailure | The transaction failed to be charged on the user's card. An associated reason will be returned with this status. |
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.
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.
Querying Transactions
For redundancy handling on your integration, it is possible to query card transaction statuses and details over the Stitch GraphQL API.
Transaction by ID
Card transactions may be looked up uniquely by the Stitch card transaction id value, as shown below:
Transaction by External Reference
Alternatively, querying by your specified externalReference may be done with a filtering query, as shown below.
This query returns an array of transactions, in case multiple card transactions have been initiated with the same externalReference.
Requesting Card Refunds
To reverse any funds back to the cardholder for a successful transaction, see our separately documented Card Refunds API calls.
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 |