Accept PaymentsServer to ServerCardNGN Card Payments

NGN Card Payments

Use the SaySwitch Server-to-Server (S2S) API to accept NGN card payments from your secure backend. Never call these endpoints from browser code because every request requires your secret key.

The flow can finish immediately or require another action. Always inspect the initialize response and follow the URL, method, and payload contract returned for that transaction.

Requirements

  • A SaySwitch secret key for the correct test or live environment
  • An active NGN wallet in the same environment
  • A unique transaction reference containing 16 to 33 characters
  • Card details collected and handled in a PCI DSS-compliant environment

The default initialize endpoint settles into your active default NGN wallet. Do not send wallet_code to it. To select a specific NGN wallet, see Card Payment With Wallet.

Endpoints

ActionMethodEndpoint
Encrypt card detailsPOSThttps://backendapi.sayswitchgroup.com/api/s2s/v1/test/encryption
Initialize paymentPOSThttps://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/initialize
Continue authenticationReturned by initializeFollow the returned authentication URL and method
Verify transactionGEThttps://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/verify/{reference}

Reference Requirement

Use the exact same reference for every stage of one payment:

  1. Card encryption
  2. Payment initialization
  3. OTP or 3DS continuation
  4. Transaction verification

The reference is used to derive the card-encryption initialization vector. A different reference causes card decryption to fail.

For the complete flow, the reference must be a string from 16 to 33 characters and must be unique for your merchant account and environment. The examples below use CARD_PAYMENT_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": "CARD_PAYMENT_2026_0001"
  }'

Encryption Fields

FieldTypeRequiredDescription
data.numberstringYesCard number. Initialization accepts 14 or more digits.
data.expiryMonthstringYesTwo-digit expiry month.
data.expiryYearstringYesTwo-digit expiry year.
data.cvvstringYesThree- or four-digit card security code.
referencestringYesThe same 16–33 character reference used for initialization.

Encryption Response

The endpoint returns the encrypted hexadecimal string directly in the response body. It does not return a JSON object.

8f2a...illustrative_hexadecimal_value...91bc

Read the response as text. The framework may label this raw text response as text/html; charset=UTF-8; the body itself is the encrypted hexadecimal card payload.

This value is not a reusable card token. Keep it only for the active transaction and any continuation step that explicitly asks for it. Never log or permanently store the card number, CVV, PIN, or encrypted card payload.

Server-Side Node.js Example

This code runs on your server with Node.js 18 or later. Do not place it in frontend JavaScript.

const reference = 'CARD_PAYMENT_2026_0001';
 
const encryptResponse = await fetch(
  'https://backendapi.sayswitchgroup.com/api/s2s/v1/test/encryption',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SAYSWITCH_SECRET_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      data: {
        number: '4005555555000009',
        expiryMonth: '05',
        expiryYear: '37',
        cvv: '000'
      },
      reference
    })
  }
);
 
if (!encryptResponse.ok) {
  throw new Error(await encryptResponse.text());
}
 
const encryptedCardHex = await encryptResponse.text();

2. Initialize the 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": "1000.00",
    "card": "ENCRYPTED_CARD_HEX_STRING",
    "currency": "NGN",
    "email": "customer@example.com",
    "reference": "CARD_PAYMENT_2026_0001"
  }'

Initialize Fields

FieldTypeRequiredDescription
amountstringYesAmount to collect. The value must be at least 1 and within your transaction and collection limits.
cardstringYesRaw hexadecimal response from the encryption endpoint.
currencystringNoUse NGN. If omitted, the API defaults to NGN.
emailstringYesValid customer email address, at least six characters.
referencestringYesUnique 16–33 character reference used during encryption.
pinstringSometimesTop-level card PIN, at least four characters, when required by the routed card flow.
statusstringSandbox onlyOptional forced result: success, failed, or pending.

Do not put pin inside the encrypted card object. The current S2S initialize controller reads PIN from the top-level pin field. Visa may not require a PIN in the current NGN flow; Mastercard, Verve, or another routed flow may require one.

The initialize response determines what happens next. Do not assume that every NGN payment requires OTP.

3. Handle the Initialize Response

Possible outcomes include immediate success, failure, OTP required, 3DS required, or processing. Response messages and authentication details vary by the route selected for the card.

Immediate Success

{
  "status": true,
  "message": "Payment is successful"
}

Another successful route may return "Payment Successful". In either case, verify the transaction before fulfilling the order.

Failure

{
  "status": false,
  "message": "Test payment declined (simulated)"
}

This is the built-in sandbox failure response. Live decline messages reflect the actual result returned for the transaction.

Processing

The sandbox forced-pending flow returns:

{
  "status": true,
  "message": "Payment Processing"
}

Treat this as pending and verify again later or wait for the webhook.

OTP Required

The NGN OTP response has this structure. Values below are illustrative placeholders.

{
  "status": true,
  "message": "Please enter transaction OTP sent to your phone number/email or enter OTP from your hardware/softtoken to complete the transaction",
  "reference": "CARD_PAYMENT_2026_0001",
  "data": {
    "paymentid": "PAYMENT_ID_RETURNED_BY_SAYSWITCH",
    "supportMessage": "Please enter transaction OTP sent to your phone number/email or enter OTP from your hardware/softtoken to complete the transaction",
    "_links": {
      "url": "RETURNED_OTP_CONTINUATION_URL",
      "method": "POST",
      "payload": ["otp, ref, payid"]
    }
  }
}

Ask the customer to enter the OTP in your interface. Your secure backend must then submit these fields to the exact data._links.url using data._links.method:

FieldValue
otpOTP entered by the customer
refCARD_PAYMENT_2026_0001
payidValue from data.paymentid
{
  "otp": "CUSTOMER_OTP",
  "ref": "CARD_PAYMENT_2026_0001",
  "payid": "PAYMENT_ID_RETURNED_BY_SAYSWITCH"
}

Do not redirect the customer to an invented OTP page and do not hardcode an opaque continuation route. Follow the URL and method returned for the transaction.

A successful OTP continuation returns:

{
  "status": true,
  "message": "Payment Successful"
}

An invalid OTP returns status: false with the message returned for that attempt.

3DS Required

The current NGN 3DS response uses separate customer and server actions:

{
  "status": true,
  "message": "Proceed authentication",
  "data": {
    "threed": {
      "TermUrl": "RETURNED_TERM_URL",
      "MD": "RETURNED_MD_VALUE",
      "ACSUrl": "RETURNED_ACS_URL",
      "jwt": "RETURNED_JWT_VALUE",
      "paymentId": "PAYMENT_ID_RETURNED_BY_SAYSWITCH",
      "transactionId": "TRANSACTION_ID_RETURNED_BY_SAYSWITCH",
      "eciFlag": "RETURNED_ECI_FLAG"
    },
    "authenticate_payer": {
      "_link": "RETURNED_CUSTOMER_AUTHENTICATION_URL",
      "_action": "web visit"
    },
    "validate_authentication": {
      "_link": "RETURNED_VALIDATION_URL",
      "_action": "api",
      "_method": "GET"
    }
  }
}

Open data.authenticate_payer._link in the customer’s browser. It renders the cardholder authentication step using the returned threed values. After the customer completes that step, your backend can call data.validate_authentication._link using its returned GET method to finalize the authentication.

Do not substitute the generic /transaction/card/3ds endpoint unless the response for that transaction explicitly returns it. The rule is simple: always follow the authentication link and method returned in the initialize response.

4. Verify the Transaction

curl --request GET \
  --url https://backendapi.sayswitchgroup.com/api/s2s/v1/transaction/verify/CARD_PAYMENT_2026_0001 \
  --header "Authorization: Bearer YOUR_SECRET_KEY" \
  --header "Accept: application/json"

The following example preserves the controller response structure. IDs, timestamps, card metadata, fees, and token values are illustrative or redacted.

{
  "success": true,
  "message": "Verification successful",
  "data": {
    "amount": "1015.00",
    "currency": "NGN",
    "status": "success",
    "transaction_date": "2026-09-25T10:31:12.000000Z",
    "reference": "CARD_PAYMENT_2026_0001",
    "domain": "test",
    "gateway_response": null,
    "channel": "card",
    "ip_address": "192.0.2.10",
    "originator_name": "",
    "originator_account_number": "",
    "fees": "15.00",
    "plan": null,
    "requested_amount": "1000.00"
  },
  "customer": {
    "id": 4512,
    "customer_code": "CUS_ILLUSTRATIVE",
    "first_name": null,
    "last_name": null,
    "email": "customer@example.com"
  },
  "card": {
    "first6Digits": "400555",
    "last4Digits": "0009",
    "expiry": "0537",
    "type": "visa",
    "token": "REDACTED"
  },
  "log": {
    "time_spent": 0,
    "attempts": 0,
    "authentication": null,
    "errors": 1,
    "success": true,
    "channel": "card",
    "history": []
  }
}

Only fulfil the order when the verified data.status is success. An initialize response, OTP result, 3DS redirect, return URL, or webhook receipt should not replace final 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 Cards

Use the normal NGN cards listed on Sandbox Test Cards. Encrypt the selected card before initialization and use the same reference for both calls.

See Sandbox Test Mode for complete examples and forced success, failure, and pending outcomes.

Errors

ConditionHTTP behaviorResponse message or shape
Missing or invalid merchant authorization401Merchant Authorization is required or Invalid Secret Key
Invalid initialize fields or reference length400status: false, validation message, and error object
Invalid hexadecimal card value or mismatched reference200 with status: falseKindly check your encrypted value
Incomplete decrypted card fields400Incomplete card request
Duplicate merchant reference200 with status: falseReference Already Exist
No active wallet for the currency and environment400Unauthorized Merchant Currency provided
Transaction limit exceeded400Transaction limit exceeded. Maximum allowed amount for NGN is ... per transaction.
Collection amount outside configured limits400Minimum or maximum collection-limit message
Invalid OTP200 with status: falseMessage returned for the OTP attempt
Missing OTP fields200 with status: falseRequest aborted. with validation errors
Transaction reference not found during verification401Reference code does not exist

Some payment-route errors are returned with HTTP 200 and status: false. Always inspect both the HTTP status and JSON body.

Full API Payload Encryption

Card-field encryption and full API payload encryption are separate layers:

  1. Card-field encryption creates the hexadecimal value sent as card.
  2. Merchants specially configured for full S2S payload encryption must then place the complete API request inside the required encrypted data envelope. Their API responses can also be returned as encrypted response bodies.

The examples on this page show the standard request format. If full payload encryption is enabled for your account, follow API Payload Encryption for the outer request and response layer.

Security Notes

  • Make every S2S call from your secure backend.
  • Keep your secret key in an environment variable or secret manager.
  • Never log or permanently store PAN, CVV, PIN, or encrypted card payloads.
  • Do not expose your secret key or card details in browser JavaScript.
  • Discard the encrypted card payload after the transaction and required continuations finish.

Legacy Endpoints

Existing integrations using /api/s2s/... can continue using the legacy family. New integrations should use /api/s2s/v1/....

ActionLegacy endpoint
Encrypt card detailsPOST /api/s2s/test/encryption
Initialize paymentPOST /api/s2s/transaction/initialize
OTP continuation, when returnedPOST /api/s2s/transaction/card/otp
3DS continuation, when returnedPOST /api/s2s/transaction/card/3ds
Verify transactionGET /api/s2s/transaction/verify/{reference}

Do not mix the /api/s2s/... and /api/s2s/v1/... endpoint families during one transaction. Use one family for encryption, initialization, any family-based continuation link, and verification. Always follow transaction-specific links returned by initialize. There is no public /api/s2s/v2/... endpoint.