Integrating Once-off Card with Stitch-Hosted UI
Once-off Card payments can be made easily by creating payment requests via the API. Users can then be guided to the returned redirect URLs, for them to complete their card payments within the Stitch-hosted UI.
This page is primarily server-side (create the payment request). Your client only needs to redirect the user to the returned URL.
Once-off Card with Hosted UI
Server-side Hosted UI once-off Card: create the payment request, redirect the user to Stitch, then handle return and webhooks.
Generate Payment Request URL
Much like Pay By Bank, Card creation is protected by a client token. You'll need to follow the steps described in the client token guide
to obtain a client token with the client_paymentrequest scope. To create a payment initiation request, you'll need to
ensure that the feature is enabled on your client.
To create the request, a GraphQL mutation is used to specify the requested amount, references and merchant information.
The GraphQL API URL https://api.stitch.money/graphql can be used for all payment requests (whether on test or live clients).
An example of a GraphQL request to create a payment initiation request with the Card payment method is shown below:
Please note that:
- The
paymentMethods.cardtype requires client configuration. Contact the technical team to assist in enabling your client for this feature. - Not setting the
paymentMethodsfield results in the Pay by Bank method being enabled on your payment request. To explicitly omit Pay by Bank as payment method, specify thepaymentMethods.eft.enabledfield with a value offalse.
Expiring payment requests
It is highly recommended that an expireAt Date (ISO 8601) is supplied in the creation of any payment initiation request. At the specified
date and time, the payment request status will automatically move to PaymentInitiationRequestExpired, if the payment is not yet successfully completed.
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.
Metadata enrichment
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.
The merchant field that was previously used for this purpose, has been deprecated. Please use the metadata field instead.
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 |
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.
Surface URL and Handle Callback
The URL returned by the API needs a redirect_uri on an allowlisted domain,
appended as a query string parameter. Send your customer to that URL and they'll be guided
through the payment request. Test clients have these domains allowlisted by default:
The allowlist covers whole domains, so any path on an allowlisted domain works. If https://example.com is on your
allowlist, then https://example.com/payment is a valid redirect_uri, and the
full URL you expose to your customer looks like this:
https://secure.stitch.money/connect/payment-request/2b068bd5-6a5a-42e1-8a45-673cb3ede612?
redirect_uri=https%3A%2F%2Fexample.com%2Fpayment
To add or remove a domain, reach out to the Stitch team.
Production clients can't use an insecure (http) redirect_uri:
Once your customer completes or cancels the payment request, they're redirected back
to the redirect_uri with these query string parameters:
| Field | Description | Type |
|---|---|---|
id | The unique Stitch ID of this payment request. | ID |
status | complete if the payment request was completed successfully, closed if the customer closed the UI, or failed if something went wrong. | String |
payment_method | The method used to complete the payment request: card or eft. | String |
externalReference | The value you supplied in the externalReference field when creating the payment request. Use it to match the payment request against your own records. | String |
Use the id to retrieve the final payment request status from the Stitch API, and to match
against incoming webhooks.
Don't use status for database operations — anyone can tamper with a query string parameter. To protect yourself from a fake payload sent to your redirect or webhook endpoint:
- Treat webhooks as your source of truth. They're always sent by Stitch.
- Use signed webhooks, so you can verify the signature on every incoming payload.
Webhooks also mean you still learn the final status of a payment request if the redirect never arrives — for example, if the request times out.
Alternative Redirect URI on Failure
Optionally, you can append a failure_redirect_uri to the URL returned from the API — e.g. ?redirect_uri=https%3A%2F%2Fexample.com%2Fpayment&failure_redirect_uri=https%3A%2F%2Fexample.com%2Fpayment%2Ffailure.
If this parameter is supplied, users will only be redirected to the redirect_uri when the status is complete, and to the failure_redirect_uri when the status is closed or failed. In other words, users that successfully complete a payment are directed to the redirect_uri, and those that exit without completing a payment are directed to the failure_redirect_uri.
The failure_redirect_uri receives the same query parameters and is subject to the same validations as the redirect_uri, including requiring the secure https:// protocol for production clients, and being validated against the configured redirect URI whitelist for your client.
Using Multiple Internal Redirect URIs
If different outcomes should land your customer on different pages, keep a single allowlisted redirect domain and branch to your internal URLs from there. An example is shown below, in NodeJS:
const status = params.status;
switch (status) {
case "complete":
redirect("/success");
break;
case "closed":
case "failed":
redirect("/retry");
break;
default:
break;
}
Cancelling Pending Payment Initiation Requests
If the user clicks the X on the dialog box, the payment initiation request will remain in the PaymentInitiationRequestPending
status. To cancel the payment request, you should call the clientPaymentInitiationRequestCancel mutation, which also
triggers the cancel webhook event.
Optionally, when the user clicks the X on the dialog box, the payment initiation request can be automatically cancelled instead. This will subsequently invoke the status update, and corresponding webhook notification.
If you would like this feature enabled on your client, reach out to the Stitch team to have it configured.
Payment Initiation Request Statuses
A Card Payment Initiation Request always starts in the PaymentInitiationRequestPending state and then transitions into exactly one terminal state. The diagram below shows the possible transitions, and the table that follows describes each status in detail.
| Status | Description |
|---|---|
| PaymentInitiationRequestCompleted | This is a final payment state, indicating the user's payment was successful. |
| PaymentInitiationRequestPending | The user hasn't yet completed the payment initiation request, or they exited the Stitch dialog box before completing the bank selection process. |
| PaymentInitiationRequestCancelled | The payment initiation request was manually cancelled by the client (or cancelled on user exit if the feature is enabled for your client). More information on this can be found here. |
| PaymentInitiationRequestExpired | The payment initiation request has expired while awaiting user interaction. More information on this can be found here. |
Subscribe to Webhooks
Since Card is similar to Pay By Bank, we subscribe to webhooks and receive payment updates in the same way.
If the subscription is successfully created, the body returned by the request will look like 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 payment webhook will be dispatched for each of the following status updates:
PaymentInitiationRequestCompletedPaymentInitiationRequestCancelledPaymentInitiationRequestExpired
Example Payload
{
"data": {
"client": {
"paymentInitiationRequests": {
"node": {
"__typename": "PaymentInitiationRequest",
"amount": {
"currency": "ZAR",
"quantity": "1"
},
"bankBeneficiaries": [
{
"__typename": "BankBeneficiary",
"accountNumber": "1234567890",
"bankAccountNumber": "1234567890",
"bankId": "nedbank",
"name": "FizzBuzz Co."
}
],
"beneficiaries": [
{
"__typename": "BankBeneficiary",
"accountNumber": "1234567890",
"bankAccountNumber": "1234567890",
"bankId": "nedbank",
"name": "FizzBuzz Co."
}
],
"beneficiaryReference": "KombuchaFizz",
"cancellationReason": null,
"created": "2022-10-11T09:52:13.312Z",
"createdAt": "2022-10-11T09:52:13.312Z",
"currency": "ZAR",
"events": [],
"externalReference": "79261d16-c53b-48eb-9019-dc9cfb6c5126",
"failureReason": null,
"feeAmount": null,
"id": "cGF5cmVxLzk2YjUyODU1LTBkMzQtNDI0MS04YmM2LWE4ODBlMDQ1ZGQzOQ==",
"payerConstraints": null,
"payerReference": "Joe-Fizz-01",
"paymentConfirmation": {
"__typename": "PaymentPending",
"date": "2022-10-11T09:52:53.128Z"
},
"netAmount": null,
"paymentMethods": [
{
"cash": {
"atm": {
"enabled": true
},
"retailer": {
"barcodeNumber": "test-11132098754321",
"barcodeUrl": "https://test-barcode-url.com",
"enabled": true
}
}
},
{
"eft": {
"enabled": true
}
}
],
"paymentRequestPayerConstraintRelationship": null,
"primarySettlement": null,
"quantity": "1",
"refunds": [],
"splitSettlements": null,
"state": {
"__typename": "PaymentInitiationRequestCompleted",
"amount": {
"currency": "ZAR",
"quantity": 1
},
"beneficiary": {
"__typename": "BankBeneficiary",
"accountNumber": "1234567890",
"bankAccountNumber": "1234567890",
"bankId": "nedbank",
"name": "FizzBuzz Co."
},
"date": "2022-10-11T09:52:53.114Z",
"id": "96b52855-0d34-4241-8bc6-a880e045dd39",
"payer": {
"__typename": "PaymentInitiationBankAccountPayer",
"accountName": "Current account",
"accountNumber": "4104754941",
"accountType": "current",
"bankId": "absa"
},
"proofOfPayment": null
},
"updated": "2022-10-11T09:52:53.128Z",
"updatedAt": "2022-10-11T09:52:53.128Z",
"url": "https://secure-local.stitchmoney.com/connect/payment-request/96b52855-0d34-4241-8bc6-a880e045dd39",
"userReference": "Joe-Fizz-01"
}
}
}
}
}
Split settlements
For clients with split settlements enabled, the payment initiation request node for a split card payment includes the settlement allocation and fee breakdown. 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 at the top level of the node, 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" }
}
]
}
primarySettlement and splitSettlements are null on an ordinary card payment. 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.
Retrieving Payment Request Status
The status of a Card request works much like Pay By Bank.
To determine if and how a payment request was completed, we will need to retrieve its status.
The two pieces of information you need from the response at this stage are the payment request id, and the url.
The payment request id is used to correlate responses and to look up the status of the request in the API, and so
should be retained for later usage. The url is used to enable the user to authorize the payment request, and you'll
need to redirect the user to this URL. We'll cover this in the next section.
Requesting Card Refunds
To reverse any funds back to the cardholder for any 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 |
Postman Collection
Card requests can be created and tested using the Postman collection available here. These can be used by specifying your client credentials and supplying the custom request variables where required.