Bank Sandbox API Docs

Standalone PSP/Bank sandbox provider API with deterministic scenarios, mock 3DS flow, and signed webhooks.

Bank PSP Sandbox API — API.md

Base URL: https://banksandboxapi.shake-out.com/api

This sandbox simulates the provider/bank side only (no checkout UI). It accepts an opaque encrypted_card_payload (base64 JSON with test PAN), runs a deterministic card-number-based scenario engine, enforces a strict transaction state machine, exposes mock OTP and 3DS challenge pages, and can dispatch signed webhooks.

Testing guide: SANDBOX_TESTING.md

Contract rule: Never return or log sensitive card data. The sandbox never stores decrypted PAN/CVV/expiry, and never persists the encrypted payload itself.


Contents


Auth

Controlled by env: SANDBOX_AUTH_MODE

Mode: api_key

Send header:

Mode: none

No auth header required.

Unauthorized response:

{
    "error": "UNAUTHORIZED",
    "message": "API key missing or invalid",
    "details": {}
}

Idempotency

Supported on:

Header:

Behavior:

Example conflict:

{
    "error": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency-Key was already used with a different merchant_order_id",
    "details": {
        "idempotency_key": "abc",
        "existing_merchant_order_id": "ORD-1",
        "incoming_merchant_order_id": "ORD-2"
    }
}

Money & precision


PCI and scope notes

Scope clarification

If you describe scope externally, be careful: the behavior is out-of-scope-like, but the real PCI scope depends on your full system boundary and assessor interpretation.


Errors format

All error responses must follow:

{
    "error": "ERROR_CODE",
    "message": "Human readable message",
    "details": {}
}

Common HTTP codes:


Transaction statuses


State machine

Valid transitions:

Any invalid transition returns 422 UNPROCESSABLE_ENTITY.

Example:

{
    "error": "INVALID_STATE_TRANSITION",
    "message": "Cannot refund a payment that is not CAPTURED",
    "details": {
        "transaction_id": "tx_xxx",
        "current_status": "APPROVED",
        "required_status": "CAPTURED"
    }
}

Sandbox Testing Scenarios

Set card_number/PAN in encrypted_card_payload to one of the test card numbers below to trigger a scenario. Amount is treated as a normal transaction amount. Definitions live in config/test_scenarios.php. Full matrix and samples: SANDBOX_TESTING.md.

Card Number Result Description
4111111111111111 Approved Successful authorization
4111111111111129 Insufficient Funds Declined — insufficient funds
4111111111111137 Lost Card Declined — lost card
4111111111111145 Stolen Card Declined — stolen card
4111111111111152 Blocked Card Declined — blocked card
4111111111111160 Invalid CVV 422 — invalid security code
4111111111111178 Invalid Expiry 422 — invalid expiry
4111111111111186 OTP Required OTP challenge
4111111111111194 3DS Required 3DS challenge
4111111111111202 Authentication Required SCA challenge
4111111111111210 Do Not Honor Generic issuer decline
4111111111111228 Issuer Unavailable Issuer unavailable
4111111111111236 Suspected Fraud Suspected fraud
4111111111111244 Transaction Timeout 504 after ~5s
4111111111111251 Velocity Limit Exceeded Velocity limit
4111111111111269 Restricted Card Restricted card
4111111111111277 Pickup Card Pickup card
4111111111111285 Invalid Merchant Invalid merchant
4111111111111293 System Error 500 provider error
4111111111111301 Generic Decline Unspecified decline

Unknown PANs default to Approved.


Webhooks

If webhook_url is provided, the sandbox sends payment.updated when a transaction is created or updated (approve/decline/3DS required/capture/refund).

Delivery rules:

Signing

Controlled by env:

HMAC mode

Headers:

Verification (PHP pseudo):

$timestamp = $_SERVER['HTTP_X_SIGNATURE_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_SIGNATURE'];
$payload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $payload, $secret);
hash_equals($expected, $signature);

Stripe-style mode

Header:

Verification (PHP pseudo):

[$tPart, $v1Part] = explode(',', $_SERVER['HTTP_STRIPE_SIGNATURE']);
$timestamp = explode('=', $tPart)[1];
$signature = explode('=', $v1Part)[1];
$payload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $payload, $secret);
hash_equals($expected, $signature);

In WEBHOOK_SIGNING_MODE=stripe, reject if:

Event: payment.updated

Payload:

{
    "event": "payment.updated",
    "data": {
        "transaction_id": "tx_xxx",
        "merchant_order_id": "ORD-12345",
        "status": "APPROVED|DECLINED|3DS_REQUIRED|CAPTURED|REFUNDED",
        "amount": 150.75,
        "currency": "EGP",
        "token": "tok_xxx_if_any",
        "provider_ref": "psp_xxx"
    }
}

3DS flow

  1. Client calls POST /psp/payments/authorize with a 3DS test PAN (4111111111111194) or authentication PAN (4111111111111202)
  2. Sandbox returns status=3DS_REQUIRED (or AUTHENTICATION_REQUIRED) and acs_url
  3. Browser opens acs_url (mock challenge UI)
  4. Challenge submits to POST /psp/3ds/complete/{transaction_id} with result=success|fail
  5. Sandbox updates to APPROVED or DECLINED and sends webhook payment.updated
  6. If return_url was provided, browser redirect: return_url?transaction_id=...&status=...&token=...

OTP flow

  1. Authorize with OTP test PAN 4111111111111186
  2. Response: OTP_REQUIRED, authentication_step=otp, challenge_reference, otp_session, challenge_url
  3. Complete via POST /psp/otp/complete/{transaction_id} with result=success|fail

See SANDBOX_TESTING.md.


Endpoints

GET /health

Purpose: Service health check.

Request

Response 200

{
    "ok": true,
    "service": "bank-sandbox-api",
    "time": "2026-01-26T10:00:00Z"
}

POST /psp/payments/authorize

Purpose: Create a transaction and return APPROVED, DECLINED, 3DS_REQUIRED, OTP_REQUIRED, AUTHENTICATION_REQUIRED, or simulated timeout/error based on test card PAN in encrypted_card_payload.

Request

Body:

{
    "merchant_order_id": "ORD-12345",
    "amount": 150.75,
    "currency": "EGP",
    "encrypted_card_payload": "BASE64_OR_HEX_STRING",
    "webhook_url": "https://your-system.com/webhooks/payment",
    "return_url": "https://your-system.com/payments/return"
}

Validation

Response 200 (APPROVED)

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "status": "APPROVED",
    "amount": 150.75,
    "currency": "EGP",
    "token": "tok_xxx",
    "provider_ref": "psp_xxx",
    "created_at": "2026-01-26T10:00:00Z"
}

Response 200 (DECLINED)

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "status": "DECLINED",
    "decline_code": "DO_NOT_HONOR",
    "decline_message": "Payment was declined",
    "amount": 150.75,
    "currency": "EGP",
    "created_at": "2026-01-26T10:00:00Z"
}

Response 200 (3DS_REQUIRED)

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "status": "3DS_REQUIRED",
    "acs_url": "https://banksandboxapi.shake-out.com/api/psp/3ds/challenge/tx_xxx",
    "amount": 150.75,
    "currency": "EGP",
    "created_at": "2026-01-26T10:00:00Z"
}

Response 504 (timeout test card)

{
    "error": "GATEWAY_TIMEOUT",
    "message": "Simulated timeout",
    "details": {
        "transaction_id": "tx_xxx_optional"
    }
}

Response 500 (system error test card)

{
    "error": "PROVIDER_ERROR",
    "message": "Simulated provider error",
    "details": {
        "provider_error_code": "SIMULATED_500"
    }
}

Error codes


POST /psp/payments/capture

Purpose: Capture an approved payment.

Request

Body:

{
    "transaction_id": "tx_xxx"
}

Response 200

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "status": "CAPTURED",
    "amount": 150.75,
    "currency": "EGP",
    "token": "tok_xxx",
    "provider_ref": "psp_xxx",
    "created_at": "2026-01-26T10:00:00Z"
}

Errors


POST /psp/payments/refund

Purpose: Refund a captured payment.

Request

Body:

{
    "transaction_id": "tx_xxx"
}

Response 200

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "status": "REFUNDED",
    "amount": 150.75,
    "currency": "EGP",
    "token": "tok_xxx",
    "provider_ref": "psp_xxx",
    "created_at": "2026-01-26T10:00:00Z"
}

Errors


GET /psp/3ds/challenge/{transaction_id}

Purpose: Display mock 3DS challenge page (HTML).

Request

Response

Errors


POST /psp/3ds/complete/{transaction_id}

Purpose: Complete 3DS challenge and update transaction state.

Request

Body (either):

Response 200 (JSON)

{
    "transaction_id": "tx_xxx",
    "status": "APPROVED|DECLINED",
    "token": "tok_xxx_if_any"
}

Redirect behavior (if return_url is set)

CSRF

This endpoint is excluded from CSRF protection because it is called from the mock 3DS page.

Errors


GET /psp/transactions/{transaction_id}

Purpose: Debug endpoint to view safe transaction data.

Request

Response 200

{
    "transaction_id": "tx_xxx",
    "merchant_order_id": "ORD-12345",
    "amount": 150.75,
    "currency": "EGP",
    "status": "APPROVED",
    "scenario": "approve",
    "token": "tok_xxx",
    "provider_ref": "psp_xxx",
    "webhook_url": "https://your-system.com/webhooks/payment",
    "return_url": "https://your-system.com/payments/return",
    "created_at": "2026-01-26T10:00:00Z",
    "updated_at": "2026-01-26T10:00:00Z"
}

Errors


Change log (recommended)