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.
Your server opens a session
Call POST
/with your secret key and an Idempotency-Key: the amount, the payer's email, andv1/ checkout-sessions allowed_origin, the https origin of your checkout page. Hand the session'sidandclient_secretto that page.Open a checkout session
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
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" } }Your page draws the button
Load
smp.jsand 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 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>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
/, 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.browser/ v1/ checkout-sessions/ {id}/ link Link the account the payer picked (sandbox: the test bank account)
The 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
/, which asks for the debit: abrowser/ v1/ checkout-sessions/ {id}/ complete WEBpayment 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
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" } }You confirm on your server
onSuccessruns in the payer's browser, so don't fulfil the order on it alone. Read the session with GET/:v1/ checkout-sessions/ {id} paymentis set once a payment was approved. The payment is an ordinary one withmetadata.checkout_session_idset, so its webhooks arrive as for any other.After the payer approved
The 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" } }
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 / shows the latest try.
A declined attempt leaves the session open
The 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 /, 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_originmust be an https origin, and the sandbox also acceptshttp://. On the live API it must also be on the one website your account is approved for; anywhere else answerslocalhost 400 validation_error. - No debit without the words. Complete needs a linked account (
409 bank_not_linkedotherwise) and the SHA-256 of exactly the words shown; different words are refused with422 authorization_mismatch, and nothing is charged. The button then fetches the words again with POST/and shows them.browser/ v1/ checkout-sessions/ {id}/ authorization-text - 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
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
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 / 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 /).
Open the bank sign-in (sandbox: the test bank account)
The 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
}
}
