Server-to-Server Payments
Use the S2S integration when your server owns the checkout flow and sends payment details directly to BXNK.
All Product API endpoints are served under /product-apis. Except for token creation, requests require Authorization: Bearer <accessToken>.
Crypto S2S payments creates a vault deposit address for every request.
Base URL
Sandbox: https://sandbox-api.bxnk.com/
Your live base URL is available in the Partner Portal under API Keys — it's listed at the bottom of the page.
Authentication
Generates a short-lived Product API bearer token from an API access key and secret.
POST /product-apis/auth/token
Content-Type: application/jsonParameters
| Name | Type | Required | Description |
|---|---|---|---|
accessKey | string | Yes | API access key. Must be a non-empty string. |
secret | string | Yes | API secret. Must be a non-empty string. |
Request
{
"accessKey": "test_pk_1234567890",
"secret": "test_sk_1234567890"
}Response 201
201{
"message": "Product API token generated successfully",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresAt": "2026-03-24T10:00:00.000Z"
}
}Errors
| Status | When it happens |
|---|---|
400 | accessKey or secret is missing or not a string. |
401 | API key credentials are invalid. |
500 | The token signing secret is not configured for the key's MID. |
Create a Payment
Creates a payment for the authenticated MID. Omit useHostedPaymentPage or set it to false for S2S.
POST /product-apis/payments
Authorization: Bearer <accessToken>
Content-Type: application/json
Idempotency-Key: order-1001-createIdempotency-Key is optional for crypto S2S payments, reused only to deduplicate retries with the same request body. It is required for S2S card payments — the request is rejected with 400 if it's missing.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount. Must be at least 0.000005 and have no more than 8 decimal places. |
currency | string | Yes | Currency code looked up by the backend. Must be a string with length 3..40; the service uppercases it before lookup. |
paymentMethod | string | Conditionally | One of card, crypto, or bank_transfer. Required when paymentMethods is omitted. Takes precedence if both fields are provided. |
paymentMethods | string[] | Conditionally | Non-empty array of card, crypto, or bank_transfer. The first value is used when paymentMethod is omitted. |
orderReference | string | No | Merchant order reference. |
customerReference | string | No | Merchant customer reference. |
successUrl | URL | No | Callback URL used for success events. Stored on the payment. |
failureUrl | URL | No | Callback URL used for failed, cancelled, or expired events. Stored on the payment. |
redirectUrl | URL | Conditionally | Required for S2S card payments. The customer's browser is sent here after any 3DS challenge completes. |
expiryTime | ISO 8601 string | No | Future expiry timestamp. Invalid or past values return 400. |
useHostedPaymentPage | boolean | No | Omit or set to false for S2S. |
metadata | object | Conditionally | Method-specific keys are read from this object: card details for the card provider path, asset/network for crypto, and sandbox scenario keys. Required whenever the payment method needs those keys. |
metadata.m_id | string | No | MID to create the payment for. The MID embedded in the Product API bearer token is used automatically, so this key can be omitted when calling with a Product API token; it is only consulted as a fallback when the token carries no MID. |
Card Request
Send raw card details in metadata to process the payment through BXNK's card provider. redirectUrl and the Idempotency-Key header are required for this path. Without any card fields in metadata, TEST-mode requests instead resolve via the sandbox simulator (see Sandbox Scenarios).
| Name | Type | Required | Description |
|---|---|---|---|
metadata.cardNumber | string | Yes | Card number, 12–19 digits. |
metadata.expiryMonth | string | Yes | Card expiry month, e.g. "12". |
metadata.expiryYear | string | Yes | Card expiry year, e.g. "2028". |
metadata.cvv | string | Yes | Card security code, 3–4 digits. |
metadata.paymentBrand | string | Yes | VISA or MC |
metadata.attemptThreeD | string | Yes | Only3D to force a 3DS challenge, or 3D to attempt 3DS automatically. |
metadata.cardHolder | string | Yes | Name printed on the card. |
metadata.customerName | string | Yes | Customer full name. Used when cardHolder is absent. |
metadata.customerEmail | string | Yes | Customer email. |
metadata.customerPhoneCountryCode | string | Yes | Customer phone country code, e.g. "+1". |
metadata.customerPhone | string | Yes | Customer phone number. |
metadata.customerIp | string | Yes | Customer IP address. |
metadata.customerBirthDate | string | No | Customer birth date. |
metadata.shippingCountry | string | Yes | Shipping country code. 2-letter ISO code, e.g. "DE". |
metadata.shippingCity | string | Yes | Shipping city. |
metadata.shippingState | string | Yes | Shipping state or region. |
metadata.shippingPostcode | string | Yes | Shipping postal code. |
metadata.shippingStreet1 | string | Yes | Shipping street address line 1. |
{
"amount": 10.0,
"currency": "EUR",
"paymentMethod": "card",
"useHostedPaymentPage": false,
"orderReference": "ORD-S2S-002-PSP",
"customerReference": "CUST-002",
"redirectUrl": "<your_redirect_url>",
"metadata": {
"paymentBrand": "VISA",
"attemptThreeD": "Only3D",
"cardNumber": "<cardnumber_without_space>",
"expiryMonth": "MM",
"expiryYear": "YYYY",
"cvv": "<cvv>",
"cardHolder": "John Doe",
"customerName": "John Doe",
"customerEmail": "[email protected]",
"customerPhoneCountryCode": "+91",
"customerPhone": "<mobile_number>",
"customerIp": "<customer_ip>",
"shippingCountry": "DE",
"shippingCity": "Berlin",
"shippingState": "DE",
"shippingPostcode": "123456",
"shippingStreet1": "<address>"
}
}The card scheme is inferred from the card number (Visa numbers starting with 4, Mastercard ranges 51–55 and 2221–2720); if it can't be inferred, metadata.paymentBrand is used as a fallback, and otherwise the request fails with 400 for an unsupported scheme.
Card Response 201 (with 3DS redirect)
201 (with 3DS redirect){
"message": "Payment created successfully",
"data": {
"paymentId": "PAY-a1b2c3d4e5f6",
"status": "processing",
"amount": 150.50,
"currency": "EUR",
"paymentMethod": "card",
"paymentFlow": "S2S",
"orderReference": "ORDER-1234",
"customerReference": "CUST-2002",
"hostedPaymentUrl": null,
"expiresAt": null,
"instructions": {
"type": "xoala_3ds_redirect",
"redirect": {
"url": "https://secure-checkout-sandbox.xoala.com/3ds/challenge",
"method": "POST",
"parameters": [
{ "name": "PaReq", "value": "eJxVUsFuwjAM..." },
{ "name": "TermUrl", "value": "https://api.bxnk.example/..." }
]
}
},
"transactionId": null,
"mId": "MID-ABCDEFGHIJ",
"walletAddress": null,
"qrCodeUrl": null,
"customerName": null,
"customerEmail": "[email protected]",
"customerWallet": null,
"createdAt": "2026-03-20T10:05:00.000Z",
"successUrl": null,
"failureUrl": null,
"redirectUrl": "https://merchant.example/payment/redirect",
"declineReason": null
}
}For 3DS flow please follow the process as described below:
Forward the customer's browser to instructions.redirect.url using instructions.redirect.method, submitting instructions.redirect.parameters as form fields for POST redirects. After the challenge completes, BXNK finalizes the payment and redirects the browser on to redirectUrl. Poll GET /product-apis/payments/{paymentId} or listen for webhooks to get the final status — do not infer success from the redirect alone.
If the card provider approves the payment without a challenge, instructions is null and status reflects the outcome directly (completed or failed) instead of processing.
Errors specific to the card provider path
| Status | When it happens |
|---|---|
400 | redirectUrl is missing for an S2S card payment. |
400 | No active card provider route is configured for the PSP, mode, and currency, or more than one route shares the same priority. |
503 | The card provider's response could not be determined (e.g. the request timed out after being sent) — the payment is left processing for manual reconciliation; do not retry the same card without contacting BXNK. |
Crypto Request
Crypto payments require both metadata.asset and metadata.network. The PSP must have a settlement wallet configured for the requested asset, and the requested amount must convert to at least the configured USD minimum for that currency.
{
"amount": 500,
"currency": "USDT",
"paymentMethod": "crypto",
"metadata": {
"m_id": "MID-ABCDEFGHIJ",
"asset": "USDT",
"network": "ethereum"
}
}Crypto Response 201
201{
"message": "Payment created successfully",
"data": {
"paymentId": "PAY-x9y8z7w6v5u4",
"status": "created",
"amount": 500,
"currency": "USDT",
"paymentMethod": "crypto",
"paymentFlow": "S2S",
"orderReference": null,
"customerReference": null,
"hostedPaymentUrl": null,
"expiresAt": null,
"instructions": null,
"transactionId": "tx_abc123def456",
"mId": "MID-ABCDEFGHIJ",
"walletAddress": "0x11d94ac1d603c28c03de643e5e67e66408f3ee89",
"qrCodeUrl": "data:image/png;base64,iVBORw0KGgo...",
"customerName": null,
"customerEmail": null,
"customerWallet": null,
"createdAt": "2026-03-20T10:05:00.000Z",
"successUrl": null,
"failureUrl": null,
"redirectUrl": null,
"declineReason": null
}
}walletAddress and qrCodeUrl are populated immediately — BXNK allocates a real vault deposit address on every crypto payment request, in both TEST and LIVE mode. There is no synthetic crypto sandbox outcome for S2S: the payment only progresses once BXNK's indexer observes an on-chain transfer to walletAddress. instructions remains null until that transfer is indexed, at which point it's populated with the indexed transfer event shown under Verify a Payment.
Create Payment Errors
| Status | When it happens |
|---|---|
400 | Validation fails, no MID can be resolved from the token or metadata.m_id, paymentMethod and paymentMethods are both omitted, currency is unknown, expiryTime is invalid or not in the future, or method-specific requirements are not met. |
400 | Crypto request omits metadata.network or metadata.asset, uses an unsupported asset/network, the deposit amount converts to less than the configured USD minimum, or the PSP has no settlement wallet configured for the asset. |
400 | Card S2S request omits raw card fields required by the card provider path, or redirectUrl is missing. |
400 | SEPA S2S request is submitted for a LIVE-mode API key. |
401 | Bearer token is missing, expired, or invalid. |
403 | Authenticated token is not authorized by the guard. |
503 | Crypto payment creation has been disabled for the environment. This gate applies only to crypto; there is no equivalent disable switch for card or SEPA creation. |
Retrieve a Payment
Returns payment details by ID.
GET /product-apis/payments/{paymentId}
Authorization: Bearer <accessToken>Response 200
200Returns the same payment object shape as create payment.
Payment retrieval and status-check endpoints are each limited to 5 requests per second. For provider reconciliation, use GET /product-apis/payments/{paymentId}/status-check for unfinished, failed, or expired payments when applicable; it may update the stored status.
Errors
| Status | When it happens |
|---|---|
401 | Bearer token is missing, expired, or invalid. |
403 | Authenticated token is not authorized by the guard. |
404 | Payment is not found. |
List Payments
Returns payments for the MID embedded in the authenticated token.
GET /product-apis/payments?page=1&limit=10&status=completed&paymentMethod=card¤cy=EUR&sortBy=createdAt&sortOrder=DESC
Authorization: Bearer <accessToken>Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | number | No | Defaults to 1. Minimum 1. |
limit | number | No | Defaults to 10. Minimum 10, maximum 100. |
sortBy | string | No | One of createdAt, amount, or status. Defaults to createdAt. |
sortOrder | string | No | ASC or DESC. Defaults to DESC. |
status | string | No | One of created, awaiting_otp, processing, completed, failed, expired, refunded, cancelled. |
paymentMethod | string | No | One of card, crypto, or bank_transfer. |
currency | string | No | Currency code. If no matching currency exists, the endpoint returns an empty page. |
orderReference | string | No | Exact match on the merchant order reference. |
customerReference | string | No | Exact match on the merchant customer reference. |
from | ISO 8601 string | No | Filters payments created on or after this timestamp. |
to | ISO 8601 string | No | Filters payments created on or before this timestamp. |
search | string | No | Accepted for validation but currently has no effect on the returned results — this endpoint does not apply it as a filter. |
Refund a Payment
Refunds a completed payment. Both partial and full refunds are supported.
Sandbox limitation: Partial refunds are not supported in the sandbox environment. In sandbox, refund the full payment amount only. Partial refunds will work in production.
POST /product-apis/payments/{paymentId}/refund
Authorization: Bearer <accessToken>
Content-Type: application/json
Idempotency-Key: order-1001-refundParameters
| Name | Type | Required | Description |
|---|---|---|---|
paymentId | string | Yes | Payment ID to refund. |
refundAmount | number | Yes | Amount to refund. Must be at least 0.01, have no more than 8 decimal places, and less or equal to the original payment amount. |
reason | string | No | Refund reason. |
Request
{
"refundAmount": 49.99,
"reason": "Customer requested refund"
}Response 201
201{
"message": "Refund processed successfully",
"data": {
"refundId": "REF-XYZ123ABC",
"paymentId": "PAY-a1b2c3d4e5f6",
"refundAmount": 49.99,
"refundStatus": "completed",
"reason": "Customer requested refund",
"createdAt": "2026-03-20T11:00:00.000Z",
"refundedAmount": 49.99,
"remainingRefundableAmount": 0,
"isFullyRefunded": true
}
}refundStatus is one of processing, completed, or failed. The cumulative fields show the total completed refund amount, the amount still refundable, and whether the payment is fully refunded.
Use GET /product-apis/payments/{paymentId}/refund/{refundId} to retrieve the current status of a refund. This endpoint is limited to 5 requests per second.
Errors
| Status | When it happens |
|---|---|
400 | Payment is not completed, the payment has already been fully refunded, the requested amount exceeds the remaining refundable amount, payment currency is missing, or PSP balance would become negative. |
403 | Payment MID does not match the authenticated token. |
404 | Payment is not found. |
429 | Polling rate limit exceeded. Payment retrieval, status-check, and refund-status endpoints allow up to 5 requests per second. |
502 | The card provider declined the refund. |
503 | The refund result could not be confirmed with the card provider and requires manual reconciliation. |
Webhook Endpoints
Register webhook endpoints before creating payments.
Create Webhook Endpoint
POST /product-apis/psp/webhooks
Authorization: Bearer <accessToken>
Content-Type: application/json| Name | Type | Required | Description |
|---|---|---|---|
url | URL | Yes | Public URL that receives webhook POSTs. |
mId | string | Yes | MID for this endpoint. Must belong to the authenticated PSP. |
secret | string | No | Signing secret. Must be length 6..255. If omitted, BXNK generates one. |
isActive | boolean | No | Defaults to true. |
The create response includes secret. List and status update responses do not include it.
List Webhook Endpoints
GET /product-apis/psp/webhooks
Authorization: Bearer <accessToken>Returns active and inactive endpoints for MIDs owned by the authenticated PSP, excluding soft-deleted endpoints.
Update Webhook Status
PATCH /product-apis/psp/webhooks/{endpointId}/status
Authorization: Bearer <accessToken>
Content-Type: application/json{
"isActive": false
}Rotate Webhook Secret
POST /product-apis/psp/webhooks/{endpointId}/rotate-secret
Authorization: Bearer <accessToken>Returns id, mId, the new secret, and updatedAt.
Webhook Delivery
Webhook requests are JSON POST requests. BXNK signs the exact JSON string with HMAC-SHA256 and sends the signature in x-psp-signature when a signing secret exists.
Registered active endpoints for the payment MID are used first. If none exist and DEFAULT_WEBHOOK_URL is configured, BXNK delivers to that fallback URL and signs with the PSP signing secret.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/bxnk/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-psp-signature'];
if (!signature) {
return res.status(401).send('Missing signature');
}
const expectedSignature = crypto
.createHmac('sha256', process.env.BXNK_WEBHOOK_SECRET)
.update(req.body) // pass the Buffer directly, no need to .toString() first
.digest('hex');
const expectedBuf = Buffer.from(expectedSignature, 'hex');
const receivedBuf = Buffer.from(signature, 'hex');
if (
expectedBuf.length !== receivedBuf.length ||
!crypto.timingSafeEqual(expectedBuf, receivedBuf)
) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
return res.sendStatus(200);
});Payment lifecycle events are emitted as payment.created, payment.awaiting_otp, payment.processing, payment.completed, payment.failed, payment.expired, payment.refunded, and payment.cancelled.
Sandbox Scenarios
TEST-mode card payments are decided by which test card number you submit.
Use the following sandbox card numbers:
| Card number | Brand | Expiry | CVV |
|---|---|---|---|
5332990065668865 | Mastercard | 10/26 | 872 |
4295550031726664 | Visa | 05/27 | 390 |
For S2S, submit these as metadata.cardNumber with the matching metadata.expiryMonth, metadata.expiryYear, and metadata.cvv (see Card Request). The resulting outcome (approved, declined, 3DS challenge, etc.) is whatever sandbox engine returns for that card.
SEPA S2S sandbox outcomes are driven by the debtor IBAN you submit. Use one of the following sandbox debtor IBANs:
| Debtor IBAN | Final status | Decline reason |
|---|---|---|
DE89370400440532013000 | completed | — |
DE91370400440532013000 | failed | insufficient_funds |
DE12500105170648489890 | failed | iban_invalid |
Crypto has no synthetic sandbox outcome for S2S: outcomes always depend on the real deposit address and the on-chain transfer indexed against it (see the Crypto Request section above).
Product API Quick Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /product-apis/auth/token | Generate a Product API bearer token. |
POST | /product-apis/payments | Create an HPP or S2S payment. |
GET | /product-apis/payments | List payments for the authenticated MID. |
GET | /product-apis/payments/{paymentId} | Retrieve a payment. |
GET | /product-apis/payments/{paymentId}/status-check | Check status and resume a non-terminal payment if possible. |
POST | /product-apis/payments/{paymentId}/refund | Refund a completed payment. |
GET | /product-apis/payments/{paymentId}/refund/{refundId} | Retrieve refund status. |
POST | /product-apis/psp/webhooks | Create a webhook endpoint. |
GET | /product-apis/psp/webhooks | List webhook endpoints. |
PATCH | /product-apis/psp/webhooks/{endpointId}/status | Enable or disable a webhook endpoint. |
POST | /product-apis/psp/webhooks/{endpointId}/rotate-secret | Rotate a webhook signing secret. |
GET | /product-apis/psp/balance | Get PSP balance. |
GET | /product-apis/psp/balances/assets | Get PSP asset balances. |
GET | /product-apis/psp/chargeback-config | Get PSP chargeback config. |
GET | /product-apis/payment-methods/active | List active payment methods. |
GET | /product-apis/payment-methods/psp | List the authenticated PSP's payment methods. |
GET | /product-apis/payment-methods/psp/{paymentMethodId}/currencies | List currencies assigned to a PSP payment method. |
GET | /product-apis/currencies | List currencies. |
GET | /product-apis/currencies/psp | List currencies supported by the authenticated PSP. |
POST | /product-apis/storage/presigned-upload | Generate a presigned S3 upload URL. |
GET | /product-apis/storage/presigned-download/{key} | Generate a presigned S3 download URL. |
DELETE | /product-apis/storage/delete/{key} | Delete an S3 object. |
GET | /product-apis/healthz | Check database health. |