Developer Reference

Merchant API.

Integrate FlexBuy Buy Now Pay Later into your existing store. Server-to-server checkout, refunds, settlements, and signed webhooks — without touching our admin panel.

Overview

The FlexBuy Merchant API lets your backend create BNPL checkout sessions, read sales and settlements, issue refunds, and receive signed webhooks. Everything you need to run BNPL as a native payment method on your own site — without your customers ever leaving your brand.

All endpoints use HTTPS and return JSON. Fee-free during test mode; production fees are covered by your merchant discount rate (MDR) set at onboarding.

Base URL & versioning

https://flexbuy.foundrcode.com/api/v1

The version prefix (v1) is stable. Breaking changes ship on a new prefix.

Authentication

Two auth flavors — pick the right one for the endpoint you're calling.

TypeUsed forHow to send
API keys
(server-to-server)
Widget sessions — the only surface a shopper interacts with directly. Header Authorization: Bearer sk_test_...
plus X-Merchant-Public-Key: pk_test_...
Dashboard token
(Sanctum)
Reading sales / settlements, issuing refunds, managing plans / webhooks / API keys. Header Authorization: Bearer <token> from the login+OTP flow below.

Getting API keys POST

Create a key pair from the Merchant app (Settings → API keys) or via API:

POST /api/v1/merchant/api-keys
Authorization: Bearer <dashboard-token>
Content-Type: application/json

{ "name": "Production backend", "environment": "live" }

// 201 — secret is shown ONCE
{
  "id": "…",
  "name": "Production backend",
  "public_key": "pk_live_…",
  "secret_key": "sk_live_…",    // store this immediately
  "environment": "live"
}
Secret keys never appear again.

Only the create response returns secret_key. The database only stores a bcrypt hash. Rotate by creating a new key and revoking the old one via DELETE /api/v1/merchant/api-keys/{id}.

Getting a Dashboard token

# Step 1 — request OTP
POST /api/v1/merchant/auth/login
{ "email": "[email protected]", "password": "…" }
// 200: { "otp_sent": true, "merchant_user_id": "…" }

# Step 2 — verify OTP
POST /api/v1/merchant/auth/otp/verify
{ "email": "[email protected]", "code": "123456" }
// 200: { "user": {...}, "token": "40|abcdef..." }

Use token as Authorization: Bearer … for every /api/v1/merchant/* call.

Widget sessions POST

The core BNPL integration. Your backend creates a session with the shopper's cart amount; the returned checkout_url is where the shopper completes the purchase.

POST /api/v1/widget/sessions
Authorization: Bearer sk_test_...
X-Merchant-Public-Key: pk_test_...
Content-Type: application/json

{
  "amount_cents": 12500,
  "currency": "USD",
  "order_reference": "ORD-9988",
  "customer_email": "[email protected]",
  "customer_phone": "+15551234567",
  "purchase_description": "Blue Wireless Headphones — Model X20",
  "success_url": "https://your-shop.com/thanks?order=ORD-9988",
  "cancel_url":  "https://your-shop.com/cart"
}

// 201 Created
{
  "session_token": "ws_...",
  "checkout_url":  "https://flexbuy.foundrcode.com/widget/checkout/ws_...",
  "expires_at":    "2026-07-16T10:41:11Z"
}

Redirect the shopper to checkout_url (or open it in an iframe / modal). On success FlexBuy either redirects the shopper to success_url, fires the flexbuy.success postMessage event to the parent window, or both. A loan.authorized webhook fires simultaneously.

Reading a session GET

GET /api/v1/widget/sessions/{token}
X-Merchant-Public-Key: pk_test_...

// 200
{
  "session_token": "ws_...", "amount_cents": 12500, "currency": "USD",
  "merchant": {...},
  "status": "authorized",   // initialized | eligible | ineligible | authorized | cancelled | expired
  "expires_at": "..."
}

Do not poll aggressively. The webhook is the source of truth. Poll only for reconciliation or an abandoned-cart recovery flow.

Cancelling a session POST

POST /api/v1/widget/sessions/{token}/cancel
X-Merchant-Public-Key: pk_test_...

A simpler alternative — a permanent, shareable URL bound to a fixed price. Perfect for social selling on WhatsApp / SMS / QR codes without an e-commerce integration.

POST /api/v1/merchant/payment-links
Authorization: Bearer <dashboard-token>
Content-Type: application/json

{ "title": "Blue Headphones", "amount_cents": 12500, "currency": "USD", "max_uses": 10 }

// 201:
{
  "id": "…", "slug": "blue-headphones-abc123",
  "public_url": "https://flexbuy.foundrcode.com/pay/blue-headphones-abc123",
  "is_redeemable": true,
  ...
}
GET    /api/v1/merchant/payment-links
GET    /api/v1/merchant/payment-links/{id}
PUT    /api/v1/merchant/payment-links/{id}
DELETE /api/v1/merchant/payment-links/{id}

Sales GET

GET /api/v1/merchant/sales?status=active&from=2026-01-01&to=2026-12-31&page=1
Authorization: Bearer <dashboard-token>

# Filters (all optional):
#   status     comma-separated: pending,approved,funded,active,paid_off,in_arrears,defaulted,written_off,cancelled
#   from       YYYY-MM-DD
#   to         YYYY-MM-DD
#   page       pagination cursor
GET /api/v1/merchant/sales/{loan_id}
// Returns full loan + installments[] + plan + customer
GET /api/v1/merchant/sales/export.csv
// Streams a CSV of every loan for accounting / BI pipelines

Refunds POST

POST /api/v1/merchant/refunds
Authorization: Bearer <dashboard-token>
Content-Type: application/json

{
  "loan_id": "…",
  "amount_cents": 2500,           // partial refunds allowed; must not exceed principal
  "reason": "Customer returned item"
}

// 201:
{ "id": "…", "status": "requested", "amount_cents": 2500, ... }

Refunds progress through requested → processing → processed (or failed). Poll GET /api/v1/merchant/refunds/{id} to observe the status transitions — refund.completed is a reserved webhook (see event catalog) but is not yet emitted.

GET /api/v1/merchant/refunds
GET /api/v1/merchant/refunds/{id}

Settlements (payouts) GET

GET /api/v1/merchant/settlements
GET /api/v1/merchant/settlements/{id}
// Includes gross_cents, mdr_cents (your discount rate), refunds_cents, adjustments_cents, net_cents.

Settlement cadence (daily / weekly / monthly) is set by the platform admin per merchant. A settlement.processed webhook fires when a batch is finalized and moved to paid.

Installment plan availability

GET /api/v1/merchant/plans
// Returns { configured: [ {plan, is_enabled} ], available: [...] }

PUT /api/v1/merchant/plans/{plan_id}
{ "is_enabled": true }
// Turn a specific plan on/off for your checkout.

Managing API keys

GET    /api/v1/merchant/api-keys                // list active keys (no secrets)
POST   /api/v1/merchant/api-keys                // create (see Authentication)
DELETE /api/v1/merchant/api-keys/{id}           // revoke

Webhooks — real-time event delivery

FlexBuy pushes signed HTTPS events to any URL your merchant registers. The webhook is the authoritative signal for BNPL state changes — build your fulfillment, ledger, and CRM triggers around them, not around widget polling.

Registering an endpoint POST

POST /api/v1/merchant/webhooks
Authorization: Bearer <dashboard-token>
Content-Type: application/json

{
  "url": "https://api.your-shop.com/flexbuy/webhook",
  "events": ["loan.authorized", "loan.funded", "installment.paid", "settlement.processed"]
}

// 201 — secret is returned ONCE
{
  "id": "…", "url": "…", "events": [...], "is_active": true,
  "secret": "whsec_..."   // store this; used to verify signatures
}
GET    /api/v1/merchant/webhooks
PUT    /api/v1/merchant/webhooks/{id}    { "url": "...", "events": [...], "is_active": false }
DELETE /api/v1/merchant/webhooks/{id}

Event catalog

Every delivery has this envelope:

POST https://api.your-shop.com/flexbuy/webhook
Content-Type: application/json
X-FlexBuy-Event: loan.funded
X-FlexBuy-Signature: 4b6d8a7c3e...

{
  "event": "loan.funded",
  "timestamp": "2026-07-16T09:42:57Z",
  "data": { ... event-specific payload ... }
}
EventFires whenPayload keys
loan.authorized Shopper completes widget checkout and the loan is booked. loan_id, loan_number, merchant_id, principal_cents, total_cents, currency, status, purchase_reference, disbursed_at
loan.funded Funds released to the merchant's settlement account for this sale. Same as loan.authorized.
loan.completed Final installment paid; loan closes. Same as loan.authorized.
installment.paid Any installment moves to paid (auto-debit success or in-app payment). loan_id, loan_number, installment_id, installment_number, amount_cents, paid_at
installment.missed Installment past due after all retries. loan_id, loan_number, installment_id, installment_number, due_date
settlement.processed Settlement batch is finalized and marked paid to the merchant. settlement_id, batch, gross_cents, mdr_cents, net_cents, currency
Reserved event names.

The Merchant app's webhook composer lets you subscribe to order.created, loan.cancelled, installment.due, refund.requested, refund.completed, dispute.opened, and dispute.resolved. These names are accepted for forward compatibility but are not yet emitted by the platform — don't rely on them until a release note confirms otherwise.

Verifying signatures (HMAC-SHA256)

Every delivery carries an X-FlexBuy-Signature header. It's the hex-encoded HMAC-SHA256 of the raw request body using the endpoint's secret. Verify before trusting anything.

Node.js / Express

const crypto = require('crypto');
app.post('/flexbuy/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-FlexBuy-Signature');
  const expected = crypto
    .createHmac('sha256', process.env.FLEXBUY_WEBHOOK_SECRET)
    .update(req.body)                     // raw Buffer, NOT JSON.stringify(req.body)
    .digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  // ... route by event.event ...
  res.status(200).end();
});

PHP

$signature = $_SERVER['HTTP_X_FLEXBUY_SIGNATURE'];
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('FLEXBUY_WEBHOOK_SECRET'));
if (! hash_equals($expected, $signature)) http_response_code(401);

Python (Flask)

import hmac, hashlib, os
sig = request.headers['X-FlexBuy-Signature']
expected = hmac.new(os.environ['FLEXBUY_WEBHOOK_SECRET'].encode(),
                    request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
    abort(401)

Retry policy

Any non-2xx response, timeout (> 15 s), or connection error triggers a retry. Backoff schedule:

Attempt 1  →  1 min later
Attempt 2  →  5 min later
Attempt 3  →  15 min later
Attempt 4  →  30 min later
Attempt 5  →  1 hour later
Attempt 6  →  2 hours later
Attempt 7  →  4 hours later
Attempt 8  →  8 hours later      (total tail ~ 15h 51m)

After the 8th attempt the delivery is marked failed and no further retries are made. Every attempt persists to webhook_deliveries — admins can inspect payload + response status from the Filament Webhook deliveries page and re-fire manually if needed.

Testing locally

  • Register an endpoint pointing at your local dev tunnel (ngrok, cloudflared, etc). Any HTTPS URL works — the platform doesn't ping until an event fires.
  • Trigger a widget checkout in test mode to fire loan.authorized. Complete an installment payment to fire installment.paid. Wait for the nightly settlement cron (or run php artisan flexbuy:settle manually) to fire settlement.processed.
  • The admin panel's Webhook deliveries page shows every attempt with response body and status — the fastest way to diagnose a failing endpoint.