DRAFTThe sandbox is not live yet. Every example here was returned by the platform's code in a test run on 2026-10-28, priced with example fees: no fee amount in them is an SMP price. Paths and fields can change before launch. Live keys are issued only after our bank program is approved.Sandbox not live yet · examples from a test run on 2026-10-28 · example fees

Guide

The drop-in Pay by bank button

Add bank payments to any checkout page: your server opens a session, smp.js draws the button, and the payer approves the debit at their own bank.

See the four steps

Four steps

The button takes one bank payment on any website. Your secret key never reaches the browser: your server opens a checkout session, and the page finishes it with the session's own client secret.

  1. Your server opens a session

    Call POST /v1/checkout-sessions with your secret key and an Idempotency-Key: the amount, the payer's email, and allowed_origin, the https origin of your checkout page. Hand the session's id and client_secret to that page.

    Open a checkout session

    POST /v1/checkout-sessions
    curl -X POST https://sandbox.socialmatterspay.com/v1/checkout-sessions \
      -H 'Idempotency-Key: order_10511' \
      -H 'Authorization: Bearer sk_sandbox_…' \
      -H 'Content-Type: application/json' \
      -d '{
      "amount_cents": 6400,
      "payer_email": "taylor.reed@example.com",
      "payer_name": "Taylor Reed",
      "allowed_origin": "https://northwind.example",
      "external_order_id": "order_10511",
      "metadata": {
        "cart_id": "c_7790"
      }
    }'
    The response · 201
    Response · 201
    HTTP 201
    Cache-Control: no-store
    
    {
      "data": {
        "id": "cs_588577021eb7470daccc4a7a5d722873",
        "object": "checkout_session",
        "status": "open",
        "amount_cents": 6400,
        "currency": "USD",
        "payer_email": "taylor.reed@example.com",
        "payer_name": "Taylor Reed",
        "allowed_origin": "https://northwind.example",
        "statement_descriptor": null,
        "external_order_id": "order_10511",
        "metadata": {
          "cart_id": "c_7790"
        },
        "site_id": null,
        "client_secret": "cs_588577021eb7470daccc4a7a5d722873_secret_…",
        "payment": null,
        "last_attempt": null,
        "expires_at": "2026-10-28T00:52:38.591Z",
        "completed_at": null,
        "created_at": "2026-10-28T00:22:38.591Z"
      }
    }
  2. Your page draws the button

    Load smp.js and mount the button with the session. The script holds no secret, and it calls the API only when the button is pressed.

    Your checkout page
    <!-- Your checkout page. Your server opened the session and put its id and client_secret here. -->
    <div id="pay-by-bank"></div>
    <script src="https://api.socialmatterspay.com/js/v1/smp.js"></script>
    <script>
      SMP.mount({
        container: '#pay-by-bank',
        sessionId: 'cs_…',                               // the session's id
        clientSecret: 'cs_…_secret_…',                   // its client_secret, never your secret key
        apiBase: 'https://sandbox.socialmatterspay.com', // leave out on the live API
        onSuccess: function (session) {
          // Approved. Confirm the session on your server before you fulfil the order.
        },
        onDecline: function (session) {
          // Nothing was charged. The button stays, and the payer can try another bank account.
        },
        onExit: function () {
          // The payer closed their bank's sign-in.
        },
        onError: function (error) {
          // error.code says what failed. Read the session on your server before trying again.
        }
      });
    </script>
  3. The payer reads the authorization and approves

    The payer selects Pay by bank, signs in to their bank and picks an account. The button links it with POST /browser/v1/checkout-sessions/{id}/link, which answers with the authorization words; nothing is debited yet. The button shows the words on a card with its own Pay by bank button.

    Link the account the payer picked (sandbox: the test bank account)

    The response · 200
    Response · 200
    HTTP 200
    Cache-Control: no-store
    Access-Control-Allow-Origin: https://northwind.example
    
    {
      "data": {
        "id": "cs_588577021eb7470daccc4a7a5d722873",
        "object": "checkout_session",
        "status": "open",
        "amount_cents": 6400,
        "currency": "USD",
        "merchant_name": "Northwind Supply",
        "payment": null,
        "expires_at": "2026-10-28T00:52:38.591Z",
        "authorization": {
          "object": "payment_authorization_text",
          "text": "By completing this payment, you authorize Northwind Supply to initiate a one-time electronic debit of $64.00 from your bank account ending in 9606. You may revoke this authorization at any time by contacting Northwind Supply, with the understanding that it may take up to 3 business days to process.",
          "text_sha256": "38b512686fc013eda5ab39bc0813e78e9077bfcc71a44e4abe3f6547bc7c27d2",
          "amount_description": "$64.00",
          "bank_account": {
            "bank_name": "Plaid Checking (sandbox)",
            "last_four": "9606"
          }
        }
      }
    }

    Pressing it is the payer's agreement. The button sends the SHA-256 of exactly the words it showed to POST /browser/v1/checkout-sessions/{id}/complete, which asks for the debit: a WEB payment like any other, checked by the risk checks before anything is sent to the bank. The words, their hash and the payer's IP address and browser on that request are the payment's authorization record.

    The payer approves: the session is complete

    The response · 200
    Response · 200
    HTTP 200
    Cache-Control: no-store
    Access-Control-Allow-Origin: https://northwind.example
    
    {
      "data": {
        "id": "cs_588577021eb7470daccc4a7a5d722873",
        "object": "checkout_session",
        "status": "complete",
        "amount_cents": 6400,
        "currency": "USD",
        "merchant_name": "Northwind Supply",
        "payment": {
          "id": "87cd9c43-25f8-4e35-96e4-7d42f84e286a",
          "status": "submitted"
        },
        "expires_at": "2026-10-28T00:52:38.591Z"
      }
    }
  4. You confirm on your server

    onSuccess runs in the payer's browser, so don't fulfil the order on it alone. Read the session with GET /v1/checkout-sessions/{id}: payment is set once a payment was approved. The payment is an ordinary one with metadata.checkout_session_id set, so its webhooks arrive as for any other.

    After the payer approved

    The response · 200
    Response · 200
    HTTP 200
    Cache-Control: no-store
    
    {
      "data": {
        "id": "cs_588577021eb7470daccc4a7a5d722873",
        "object": "checkout_session",
        "status": "complete",
        "amount_cents": 6400,
        "currency": "USD",
        "payer_email": "taylor.reed@example.com",
        "payer_name": "Taylor Reed",
        "allowed_origin": "https://northwind.example",
        "statement_descriptor": null,
        "external_order_id": "order_10511",
        "metadata": {
          "cart_id": "c_7790"
        },
        "site_id": null,
        "client_secret": "cs_588577021eb7470daccc4a7a5d722873_secret_…",
        "payment": {
          "id": "87cd9c43-25f8-4e35-96e4-7d42f84e286a",
          "status": "submitted",
          "amount_cents": 6400
        },
        "last_attempt": {
          "id": "87cd9c43-25f8-4e35-96e4-7d42f84e286a",
          "status": "submitted",
          "amount_cents": 6400
        },
        "expires_at": "2026-10-28T00:52:38.591Z",
        "completed_at": "2026-10-28T00:22:38.637Z",
        "created_at": "2026-10-28T00:22:38.591Z"
      }
    }

The button

Drawn by smp.js itself, on a light page and on a dark one (ground: 'dark'). Shown inert: these two are not connected to a session.
Options of SMP.mount(), as smp.js reads them.
OptionWhat it does
containerRequired. The element, or a CSS selector for it, that the button goes into.
sessionIdRequired. The session's id.
clientSecretRequired. The session's client_secret. The script refuses anything else, your secret key included.
apiBasehttps://api.socialmatterspay.com unless you set it. Set https://sandbox.socialmatterspay.com to test.
sizes, m or l: 40, 48 or 56 pixels tall. m unless you set it.
groundlight or dark: the background the button sits on. light unless you set it.
onSuccess(session)A payment was approved and the session is complete.
onDecline(session)The payment was declined and nothing was charged. The session stays open for another bank account.
onExit()The payer closed their bank's sign-in before choosing an account.
onError(error)A request failed; error.code says why. Read the session on your server before you offer the button again.

SMP.mount() returns element and destroy(), which removes the button. The button and its status line are written in plain words for the payer, including what to do when their bank's sign-in is blocked by iCloud Private Relay.

When a payment is declined

A decline is not an error. The complete request answers 200 with the session still open, the declined payment and a sentence to show the payer; nothing was charged. The payer can choose another bank account on the same session, and the first approved payment completes it. last_attempt on GET /v1/checkout-sessions/{id} shows the latest try.

A declined attempt leaves the session open

The response · 200
Response · 200
HTTP 200
Cache-Control: no-store
Access-Control-Allow-Origin: https://northwind.example

{
  "data": {
    "id": "cs_0426a0788d9146648a15cd1da2c1227b",
    "object": "checkout_session",
    "status": "open",
    "amount_cents": 450000,
    "currency": "USD",
    "merchant_name": "Northwind Supply",
    "payment": {
      "id": "56648fef-8a63-4df6-8a51-64f6c64452fd",
      "status": "declined"
    },
    "expires_at": "2026-10-28T00:52:38.614Z",
    "message": "Your bank payment wasn't approved, and nothing was charged. Choose another way to pay."
  }
}

A session ends when a payment is approved, when you cancel it with POST /v1/checkout-sessions/{id}/cancel, or at expires_at: 30 minutes after it opened unless you set expires_in_seconds (5 minutes to 24 hours).

What keeps it safe

  • Your secret key stays on your server. The client secret belongs to one session and can't pay once that session has ended.
  • The browser calls answer only pages on allowed_origin; a page on any other origin is refused. allowed_origin must be an https origin, and the sandbox also accepts http://localhost. On the live API it must also be on the one website your account is approved for; anywhere else answers 400 validation_error.
  • No debit without the words. Complete needs a linked account (409 bank_not_linked otherwise) and the SHA-256 of exactly the words shown; different words are refused with 422 authorization_mismatch, and nothing is charged. The button then fetches the words again with POST /browser/v1/checkout-sessions/{id}/authorization-text and shows them.
  • A session makes at most one approved payment. If two tabs press the button at once, one request goes ahead and the other answers 409.

Preflight from any other origin is refused

OPTIONS /browser/v1/checkout-sessions/cs_588577021eb7470daccc4a7a5d722873/link-token
curl -X OPTIONS https://sandbox.socialmatterspay.com/browser/v1/checkout-sessions/cs_588577021eb7470daccc4a7a5d722873/link-token \
  -H 'Origin: https://elsewhere.example' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'
The response · 403
Response · 403
HTTP 403

In the sandbox

Set apiBase to the sandbox host and use a sandbox key on your server. Where the sandbox has no bank sign-in configured, POST /browser/v1/checkout-sessions/{id}/link-token answers sandbox_link: true, as in this example, and the button links a test bank account for the session's payer instead. To see a decline, open a session above your per-payment limit (limits.per_transaction_cents on GET /v1/account).

Open the bank sign-in (sandbox: the test bank account)

The response · 200
Response · 200
HTTP 200
Cache-Control: no-store
Access-Control-Allow-Origin: https://northwind.example

{
  "data": {
    "object": "link_token",
    "sandbox_link": true,
    "link_token": null,
    "private_relay": null
  }
}