Social Pay Social Pay

Checkout API · Version 1

Payments that turn every buyer into a channel.

Social Pay is a payment method that adds automatic cashback and a viral referral link on top of a licensed acquirer. One server-to-server API creates a hosted checkout, confirms payment through signed webhooks, and settles refunds — you never touch card data or hold funds.

Self-contained sandbox Signed webhooks Idempotent refunds
Base URL https://fzttibmnhhhigvpuqnky.supabase.co/functions/v1/socialpay-checkout

How a payment flows

  1. Create a session from your server with an API key, then redirect the buyer to the returned checkout_url.
  2. The buyer pays on the acquirer's hosted, PCI-compliant page (with 3-D Secure).
  3. Social Pay confirms the payment server-to-server and fires a signed checkout.session.completed webhook — the browser never confirms a charge.
  4. The buyer earns cashback and a personal referral link, and is returned to your return_url.

Get started

Authentication

Server-to-server endpoints authenticate with a merchant API key sent as a bearer token. The merchant identity is derived from the key — you never pass a merchant id.

http
Authorization: Bearer sp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • sp_live_… keys act on your production merchant; sp_test_… keys act on the shared sandbox merchant. A key can never cross environments.
  • Keys are created in the merchant dashboard and shown once — only a SHA-256 hash is stored. Rotate by creating a new key and revoking the old one.
  • Scopes: create_session, read_status, and refund (opt-in, enabled per key).

Session-scoped reads (GET /session/{id}, /payment-intent, /status/{id}, /simulate) are authorized by the unguessable session id itself and take no bearer token.

Get started

Base URL & versioning

All requests are made to a single production base URL. This is API v1; breaking changes ship under a new major version, never in place.

Machine-readable spec
OpenAPI 3.1 — import into Postman, Insomnia, or a client generator.
Content type
application/json on every request and response.

Concepts

Money & amounts

All amounts are decimal euros with at most two decimal places (for example 19.90), rounded half-up. A value with finer precision is rejected rather than silently rounded, so the amount on the wire always equals the amount stored. currency is an ISO-4217 code and defaults to EUR.

On POST /session, order.total must equal the sum of the line items within ±0.01.

Concepts

Rate limits

Requests are limited per fixed 60-second window. Authenticated endpoints are limited per merchant (not per key, so minting extra keys can't multiply your budget); the public simulator is limited per client IP.

EndpointLimitScope
POST /session300 / minper merchant + environment
POST /refunds60 / minper merchant + environment
POST /session/{id}/simulate60 / minper client IP

Over the limit returns 429 rate_limited with a Retry-After header (seconds) and a retry_after field. Back off and retry after that delay.

Concepts

Errors

Errors use standard HTTP status codes and a JSON body of the shape { "error": "…", "message": "…" }. Validation errors carry a details array instead.

StatusErrorMeaning
400Validation error · invalid_amountMalformed body, or a refund amount that is negative, over the remaining balance, or has more than 2 decimals.
401invalid_api_keyMissing or invalid bearer key.
403insufficient_scope · test_key_on_live_merchant · sandbox_only · not_a_sandbox_sessionThe key lacks a scope, or key and merchant environments don't match.
404*_not_foundNo such session, payment, or merchant.
409payment_not_refundable · idempotency_key_in_progressSession not payable, payment not refundable, or a concurrent request holds the idempotency key.
410session_expiredThe session passed its expiry (on reads that don't poll).
422idempotency_key_reusedThe idempotency key was reused with a different request body.
429rate_limitedToo many requests — honour Retry-After.
503sandbox_unavailableA sandbox payment-intent was requested before the xMoney sandbox is wired — use the simulator instead.

Concepts

Sandbox & test cards

An sp_test_ key creates sessions on a shared sandbox merchant. Sandbox sessions are completed with a self-contained simulator (POST /session/{id}/simulate) — no real money, no card network, no acquirer dependency. Every record is flagged demo and is excluded from payouts and analytics; webhooks carry livemode: false.

Never send real card or personal data to the sandbox. A test key can only ever operate on a sandbox merchant — it can never trigger a real charge or refund.
Card numberOutcome
4242 4242 4242 4242Success
4000 0000 0000 0002Declined
4000 0000 0000 9995Insufficient funds

Endpoints

Create a session

POST/sessionAuth: API key · scope create_session

Create a checkout session and receive a hosted checkout_url to redirect the buyer to.

Body parameters

FieldDescription
order.items[]requiredLine items: product_id, name, price, quantity, optional product_url.
order.totalrequiredOrder total in decimal euros; must equal the sum of items (±0.01).
order.currencyoptionalISO-4217, defaults to EUR.
order.referral_codeoptionalAttributes the sale to a referring ambassador.
customeroptionalemail, phone, first_name, last_name — may be completed later.
return_urlrequiredWhere the buyer is sent after checkout. Must be an http(s) URL.
cURL
curl -X POST "$BASE/session" \
  -H "Authorization: Bearer sp_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "total": 19.90,
      "currency": "EUR",
      "items": [{ "product_id": "SKU1", "name": "Demo tee", "price": 19.90, "quantity": 1 }]
    },
    "customer": { "email": "buyer@example.com" },
    "return_url": "https://your-store.example/thanks"
  }'

Response · 201

json
{
  "session_id": "chk_7oztnk9kh2lvlwbv",
  "checkout_url": "https://checkout.social-pay.io/checkout/session/chk_7oztnk9kh2lvlwbv"
}

Endpoints

Retrieve a session

GET/session/{id}Auth: session id

Return the public view of a session — what the hosted checkout page renders. Authorized by the session id alone, so it never exposes the merchant uuid or PII beyond the buyer email.

json
{
  "session_id": "chk_7oztnk9kh2lvlwbv",
  "status": "pending",
  "merchant": { "name": "AMO Paris", "logo_url": "https://…", "return_url": "https://…" },
  "items": [ { "product_id": "SKU1", "name": "Demo tee", "price": 19.90, "quantity": 1 } ],
  "currency": "EUR",
  "subtotal": 19.90, "shipping": 0, "tax": 0, "total": 19.90,
  "customer_email": "buyer@example.com",
  "expires_at": "2026-07-24T12:30:00Z"
}

Returns 410 session_expired for a pending session past its expiry.

Endpoints

Payment intent

GET/session/{id}/payment-intentAuth: session id

Return the descriptor the hosted page needs to mount the payment form. For a live session this is the signed acquirer order; for a sandbox session it's a test-mode descriptor pointing at the simulator.

json — sandbox test mode
{
  "test_mode": true,
  "simulate_url": "/socialpay-checkout/session/chk_…/simulate",
  "test_cards": { "success": "4242424242424242", "declined": "4000000000000002", "insufficient_funds": "4000000000009995" }
}

Returns 409 if the session isn't payable, 410 if expired, and 503 sandbox_unavailable if a sandbox intent is requested before the xMoney sandbox is configured.

Endpoints

Simulate a payment

POST/session/{id}/simulateAuth: session id · sandbox only

Complete or decline a sandbox session deterministically by test card, running the real post-payment pipeline (order, cashback, webhooks) with everything flagged demo. Returns 403 not_a_sandbox_session on a live session. A declined card leaves the session pending so you can retry with a success card.

cURL
curl -X POST "$BASE/session/chk_…/simulate" \
  -H "Content-Type: application/json" \
  -d '{ "card": "4242424242424242" }'
json — 200
{
  "status": "success",
  "session_id": "chk_…",
  "order_id": "ORD-…",
  "payment_id": "PAY-SBX-…",
  "share_id": "shr_…",
  "test_mode": true
}

Endpoints

Poll status

GET/status/{id}Auth: session id

Poll a session's status. The signed webhook is the authoritative signal — polling is a fallback. Poll until status reaches a terminal state.

FieldDescription
statusOne of pending, processing, success, failed, expired.
order_idPresent on success.
share_idReferral share id, present on success.
error_codepayment_declined (failed) or session_expired (expired). An expired session is reported with HTTP 200, not 410, so pollers get a terminal answer.
json — success
{ "status": "success", "order_id": "ORD-…", "share_id": "shr_…" }

Endpoints

Refund a payment

POST/refundsAuth: API key · scope refund

Refund a payment in full or in part. Idempotent via the Idempotency-Key header, which is bound to the request body: replaying a key returns the stored response, and reusing it with a different body returns 422. Omit amount to refund the full remaining balance.

Body parameters

FieldDescription
payment_idrequiredThe payment to refund. Must belong to the key's merchant.
amountoptionalDecimal euros, ≤2dp. Omit for the full remaining balance.
reasonoptionalFree-text reason, stored on the refund event.
cURL
curl -X POST "$BASE/refunds" \
  -H "Authorization: Bearer sp_test_…" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "payment_id": "PAY-SBX-…", "amount": 5.00 }'
json — 200
{
  "refund_event_id": "a1b2…",
  "payment_id": "PAY-SBX-…",
  "amount": 5.00,
  "is_full_refund": false,
  "status": "sandbox_dry_run"
}

On a live payment the status is pending_provider; in the sandbox it's sandbox_dry_run. A successful refund also emits a payment.refunded webhook.

Webhooks

Events & delivery

Register HTTPS endpoints in the dashboard under Integrations → Webhooks. Each delivery is signed, retried on non-2xx responses with exponential backoff, and carries a livemode flag so you can filter test traffic.

EventFired when
checkout.session.completedA payment completes (the authoritative confirmation).
payment.refundedA refund is recorded.
webhook.pingYou click Send test — use it to verify your signature check before going live.

Webhooks

Verify a signature

Every delivery carries an X-SocialPay-Signature header of the form t=<unix>,v1=<hex>, where v1 is an HMAC-SHA256 of t + "." + rawRequestBody keyed with your endpoint's signing secret.

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const { t, v1 } = parse(header); // "t=…,v1=…"
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`).digest("hex");
  if (!timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) throw new Error("bad signature");
  if (Date.now() / 1000 - Number(t) > 300) throw new Error("stale timestamp");
  return JSON.parse(rawBody);
}
Compute v1 over the raw body before any JSON parsing, reject a stale t, and derive the event type and idempotency key from the signed body (id, type) — not from the advisory X-SocialPay-* headers.