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, runs a deterministic scenario engine (approve/decline/3DS/timeout/error), enforces a strict transaction state machine, exposes mock 3DS challenge pages, and can dispatch signed webhooks.

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"
    }
}

Scenarios

Scenario engine is deterministic. You can force behavior via request field:

Notes:


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 scenario=3ds
  2. Sandbox returns status=3DS_REQUIRED and acs_url
  3. Browser is redirected to acs_url (mock challenge UI)
  4. Challenge submits result to POST /psp/3ds/complete/{transaction_id} with result=success|fail
  5. Sandbox updates status to APPROVED or DECLINED and sends webhook payment.updated
  6. If return_url was provided, the sandbox redirects the browser to:

return_url?transaction_id=...&status=...&token=... (token only if approved)


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, or 3DS_REQUIRED (or simulated timeout / error).

Request

Body:

{
    "merchant_order_id": "ORD-12345",
    "amount": 150.75,
    "currency": "EGP",
    "encrypted_card_payload": "BASE64_OR_HEX_STRING",
    "scenario": "approve|decline|3ds|timeout|error",
    "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 (scenario=timeout)

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

Response 500 (scenario=error)

{
    "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)