USD Card Payments
USD card payments use the current S2S v1 card endpoint. Set currency to USD; the initialize response tells your backend whether the payment succeeded, failed, or requires OTP or 3DS authentication.
All requests must come from your secure backend. Never expose your SaySwitch secret key in browser JavaScript.
Requirements
- USD collection must be enabled for your business.
- Your business must have an active USD wallet in the correct test or live environment.
- The current international-card flow supports Visa and Mastercard.
- Use a unique transaction reference containing 16 to 33 characters.
- Provide a merchant-owned return URL when no callback URL is already configured for your account.
Endpoints
| Action | Method | Endpoint |
|---|---|---|
| Encrypt card details | POST | https://backendapi.sayswitchgroup.com/api/s2s/v1/test/encryption |
| Initialize payment | POST | https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/initialize |
| Complete OTP | POST | Returned in _links.url; normally the v1 OTP continuation endpoint |
| Complete 3DS | POST | Returned in _links.url; normally the v1 3DS continuation endpoint |
| Verify transaction | GET | https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/verify/{reference} |
Use the exact same reference for encryption, initialization, OTP or 3DS continuation, and verification. The reference is used to derive the card-encryption initialization vector, so changing it prevents decryption.
The examples use USD_CARD_2026_0001 throughout.
1. Encrypt Card Details
curl --request POST \
--url https://backendapi.sayswitchgroup.com/api/s2s/v1/test/encryption \
--header "Authorization: Bearer YOUR_SECRET_KEY" \
--header "Content-Type: application/json" \
--data '{
"data": {
"number": "4005555555000009",
"expiryMonth": "05",
"expiryYear": "37",
"cvv": "000"
},
"reference": "USD_CARD_2026_0001"
}'| Field | Type | Required | Description |
|---|---|---|---|
data.number | string | Yes | Visa or Mastercard card number. |
data.expiryMonth | string | Yes | Two-digit expiry month. |
data.expiryYear | string | Yes | Two-digit expiry year. |
data.cvv | string | Yes | Three- or four-digit card security code. |
reference | string | Yes | The same 16–33 character reference used for initialization. |
The response is the encrypted hexadecimal string itself, not JSON:
8f2a...illustrative_hexadecimal_value...91bcRead the response body as text. The framework may return text/html; charset=UTF-8 for this raw text response.
The encrypted card value is transaction-specific, not a reusable token. Keep it only while the payment and any required 3DS continuation are active. Never log or permanently store raw card details or the encrypted payload.
Do not add PIN inside the encrypted card object for initialization. The current outer S2S controller does not forward an encrypted inner PIN to the international-card flow.
2. Initialize the USD Payment
curl --request POST \
--url https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/initialize \
--header "Authorization: Bearer YOUR_SECRET_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{
"amount": "10.00",
"card": "ENCRYPTED_CARD_HEX_STRING",
"currency": "USD",
"email": "customer@example.com",
"reference": "USD_CARD_2026_0001",
"callbackUrl": "https://merchant.example.com/payment/return"
}'Initialize Fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | USD amount, at least 1 and within your configured limits. |
card | string | Yes | Raw hexadecimal response from the card encryption endpoint. |
currency | string | Yes | Use USD. |
email | string | Yes | Valid customer email address. |
reference | string | Yes | Unique 16–33 character reference used during encryption. |
callbackUrl | URL | Conditional | Your return URL. Required when your merchant account does not already have a callback URL configured. |
pin | string | No | Top-level PIN, four to six characters, when required by the card flow. |
The backend also recognizes returnUrl, return_url, and callback_url, but callbackUrl is recommended for consistency.
callbackUrl belongs to the merchant. It is used to return the customer’s browser after authentication and has the transaction reference appended as a query parameter. A return to this URL is not proof of payment; always verify the transaction.
If a PIN is needed, send it only as the top-level pin field. Do not claim or depend on PIN inside the encrypted card object.
3. Handle the Initialize Response
Inspect the actual response. Do not select OTP or 3DS yourself.
Immediate Success
{
"status": true,
"message": "Payment Successful"
}Verify the transaction before fulfilling the order.
Failure
{
"status": false,
"message": "Unable to process card payment"
}The message is replaced by the actual validation, configuration, or decline message when one is available.
OTP Required
The international-card flow normalizes an OTP action into this structure. Placeholder values are illustrative.
{
"status": true,
"message": "OTP authorization required",
"data": {
"auth": "otp",
"otp": {
"message": "Please enter the OTP sent to the cardholder",
"tokenId": "TOKEN_ID_RETURNED_BY_SAYSWITCH"
}
},
"_links": {
"url": "https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/card/otp",
"method": "POST",
"payload": ["ref", "otp"]
}
}The _links object is top-level in this response. Other card routes can place continuation links elsewhere, so do not merge response formats. Follow the exact _links.url, _links.method, and payload returned for the transaction.
Ask the customer for the OTP, then submit it from your backend:
curl --request POST \
--url https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/card/otp \
--header "Authorization: Bearer YOUR_SECRET_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{
"ref": "USD_CARD_2026_0001",
"otp": "CUSTOMER_OTP"
}'The backend retains the authorization token ID returned during initialization. Merchants normally send only ref and otp, as specified by _links.payload.
A successful continuation returns:
{
"status": true,
"message": "Payment Successful"
}An invalid OTP returns status: false with the message returned for that attempt.
3DS Required
{
"status": true,
"message": "3DS authorization required",
"data": {
"auth": "3ds",
"threed": {
"id": "RETURNED_3DS_ID",
"redirectUrl": "RETURNED_REDIRECT_URL",
"acsUrl": "RETURNED_ACS_URL",
"method": "POST",
"payload": {
"creq": "RETURNED_CREQ_VALUE",
"TermUrl": "RETURNED_TERM_URL"
},
"termUrl": "RETURNED_TERM_URL",
"callBackUrl": "RETURNED_CALLBACK_URL",
"orderId": "RETURNED_ORDER_ID",
"transactionId": "RETURNED_TRANSACTION_ID",
"paymentId": "RETURNED_PAYMENT_ID"
}
},
"_links": {
"url": "https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/card/3ds",
"method": "POST",
"payload": ["ref", "card"]
}
}The backend returns all data.threed keys shown above. A value can be null when it was not supplied for that authentication attempt. method defaults to GET if no method is returned.
Present the cardholder challenge using data.threed.acsUrl or data.threed.redirectUrl, the returned method, and every value in data.threed.payload. After the customer returns to your callbackUrl, continue using the top-level _links contract.
The current 3DS continuation requires the same encrypted card payload:
curl --request POST \
--url https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/card/3ds \
--header "Authorization: Bearer YOUR_SECRET_KEY" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{
"ref": "USD_CARD_2026_0001",
"card": "ENCRYPTED_CARD_HEX_STRING"
}'Use the returned URL rather than constructing it yourself. Keep the encrypted payload only until this continuation has finished.
Missing stored 3DS authorization data returns HTTP 400:
{
"status": false,
"message": "3DS authorization data not found"
}4. Verify the Transaction
curl --request GET \
--url https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/verify/USD_CARD_2026_0001 \
--header "Authorization: Bearer YOUR_SECRET_KEY" \
--header "Accept: application/json"The verification response uses the same structure documented for NGN card verification, with data.currency set to USD.
Only fulfil the order when the verified data.status is success. An initialize response, OTP result, 3DS browser return, callback URL, or webhook is not a substitute for server-side verification.
If the reference does not exist for the authenticated merchant and environment, the endpoint returns HTTP 401:
{
"success": false,
"message": "Reference code does not exist"
}Sandbox Testing
The cards on Sandbox Test Cards are documented for the normal NGN S2S simulator. For USD testing, use the sandbox details issued for your USD configuration.
See Sandbox Test Mode for the validated status override behavior.
Errors
| Condition | HTTP behavior | Response message or shape |
|---|---|---|
| Missing or invalid merchant authorization | 401 | Merchant Authorization is required or Invalid Secret Key |
| Invalid initialize fields or reference length | 400 | status: false, validation message, and error object |
| Invalid hexadecimal card value or mismatched reference | 200 with status: false | Kindly check your encrypted value |
| Incomplete decrypted card fields | 400 | Incomplete card request |
| Unsupported card brand | 400 | USD international card payments currently support Visa and Mastercard only |
| Duplicate merchant reference | 200 with status: false | Reference Already Exist |
| USD collection disabled | 403 | USD collection is not enabled for this business |
| No active USD wallet in the environment | 400 | Unauthorized Merchant Currency provided |
| Transaction amount limit exceeded | 400 | Transaction limit exceeded. Maximum allowed amount for USD is ... per transaction. |
| Collection amount outside configured limits | 400 | Minimum or maximum collection-limit message |
| Missing callback URL and no configured merchant callback | 400 | callbackUrl is required for USD S2S card payments when no merchant callback URL is configured |
| Missing OTP authorization data | 400 | OTP authorization data not found |
| Missing 3DS authorization data | 400 | 3DS authorization data not found |
| Invalid OTP or failed authentication | 200 with status: false | Message returned for the authentication attempt |
| Transaction reference not found during verification | 401 | Reference code does not exist |
Some card-route failures return HTTP 200 with status: false. Inspect both the HTTP status and JSON body.
Full API Payload Encryption
There are two independent encryption layers:
- Card-field encryption produces the hexadecimal value sent as
card. - Merchants configured for full S2S payload encryption must encrypt the entire request into the outer
dataenvelope and decrypt the encrypted response body.
The examples above show the standard request format. See API Payload Encryption if the second layer is enabled for your account.
Security Notes
- Keep secret keys and card processing on your secure backend.
- Never expose secret keys in frontend JavaScript.
- Never log or permanently store PAN, CVV, PIN, or encrypted card payloads.
- Keep the encrypted payload only for the active transaction and a continuation step that requests it.
- Verify
data.statusbefore providing goods or services.
Legacy Endpoints
Existing /api/s2s/... integrations can continue using the legacy family. New integrations should use /api/s2s/v1/....
| Action | Legacy endpoint |
|---|---|
| Encrypt card details | POST /api/s2s/test/encryption |
| Initialize payment | POST /api/s2s/transaction/initialize |
| Complete OTP | POST /api/s2s/transaction/card/otp |
| Complete 3DS | POST /api/s2s/transaction/card/3ds |
| Verify transaction | GET /api/s2s/transaction/verify/{reference} |
Do not mix legacy and v1 endpoints during one transaction. Encryption, initialization, authentication continuation, and verification must use the same endpoint family. Always prefer the continuation URL returned by the transaction. There is no public /api/s2s/v2/... endpoint.