F
Developer guide
Custom sites & Lovable

Add interest-free payment plans to a custom-built store

FlexiLay works with any website that can make a server-side HTTP call — including sites built with Lovable. Your site creates a plan through the FlexiLay API and sends the customer to a FlexiLay-hosted payment page. Cards, deposits, instalments, retries and receipts are all handled by FlexiLay; your site never touches card data.

Your checkoutServer-side call creates a plan for the order
→
FlexiLay setup pageCustomer picks deposit & schedule, pays via Stripe
→
Auto-chargingFlexiLay collects instalments until paid in full
→
Your site notifiedSigned webhook + status API on every change
1

Set up your FlexiLay merchant account

One-time setup, no code:

Register at my.flexilay.com/auth/register using your store's domain. Once your account is approved, connect Stripe and complete onboarding from the dashboard — payments settle directly into your own Stripe account.

Your API key (starts with fl_) is in the dashboard under Settings → API Key. Treat it like a password.

Keep the API key server-side. In Lovable, store it as a backend secret and call FlexiLay from an edge function (Lovable Cloud / Supabase). Calls from the browser will fail (cross-origin blocked) and would expose your key.

2

Create a plan when the customer checks out

When the customer chooses FlexiLay at your checkout, make one server-side request. Amounts are integer cents.

POST https://my.flexilay.com/api/plans/draft
X-Flexilay-Shop: store.example.com
X-Flexilay-Api-Key: fl_xxxxxxxx...
Content-Type: application/json

{
  "amountCents": 25000,          // $250.00
  "currency": "AUD",
  "customerName": "Alex Smith",
  "customerEmail": "alex@example.com",
  "customerMobile": "+61400000000",
  "externalOrderId": "1001",     // your order ID — echoed back in webhooks
  "externalOrderName": "#1001",
  "completeBy": "2026-12-01",    // optional payment deadline — see below
  "checkoutUrl": "https://store.example.com/checkout",  // optional — enables "Cancel & return"
  "lineItems": [
    { "title": "Product name", "quantity": 1, "priceCents": 25000 }
  ]
}

The response includes the hosted payment page URL:

{
  "ok": true,
  "planId": "ckw…",
  "publicToken": "6f0c…",
  "planUrl": "https://my.flexilay.com/storefront/plan/setup/6f0c…",
  "completeBy": "2026-12-01"     // null when no deadline applied
}

Optional: a payment deadline (completeBy)

For date-sensitive orders — event tickets, bookings, hire — pass completeBy as a YYYY-MM-DD date and FlexiLay limits the customer's schedule options so every instalment falls on or before that date. Frequencies that can't fit are hidden (e.g. monthly disappears on a three-week deadline), the instalment slider is capped per frequency, and the setup page shows the customer "This plan must be fully paid by …". If the date is too close for any instalment plan, the customer is simply asked to pay in full instead — the sale is never rejected.

  • Omit it (or send null) for no deadline — the customer gets your normal schedule options exactly as before.
  • Past or invalid dates are ignored, never an error: the plan is created without a deadline and the response echoes "completeBy": null. Your checkout can't fail over a bad date.
  • The limits are enforced server-side when the customer confirms — a stale or tampered setup page can't schedule past the date.
  • The deadline shapes the schedule; if a later instalment fails, normal retry and overdue handling still apply past the date.

Example: Lovable / Supabase edge function

// supabase/functions/create-flexilay-plan/index.ts
// Secrets: FLEXILAY_SHOP, FLEXILAY_API_KEY
Deno.serve(async (req) => {
  const order = await req.json(); // validate & price the order server-side

  const r = await fetch("https://my.flexilay.com/api/plans/draft", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Flexilay-Shop": Deno.env.get("FLEXILAY_SHOP")!,
      "X-Flexilay-Api-Key": Deno.env.get("FLEXILAY_API_KEY")!,
    },
    body: JSON.stringify({
      amountCents: order.totalCents,
      currency: "AUD",
      customerName: order.name,
      customerEmail: order.email,
      externalOrderId: order.id,
      lineItems: order.items,
    }),
  });

  const data = await r.json();
  if (!data.ok) return new Response(JSON.stringify(data), { status: 502 });

  // Send the browser to data.planUrl
  return Response.json({ planUrl: data.planUrl, planId: data.planId });
});
3

Send the customer to the payment page

Redirect the browser to planUrl. The customer picks their deposit, payment frequency and number of instalments, then pays the deposit securely (Stripe Elements, Apple Pay / Google Pay supported). After the deposit, the plan activates and FlexiLay auto-charges the remaining instalments — including retries, overdue reminders and receipts.

Give customers a way back to your checkout (checkoutUrl)

Pass checkoutUrl when creating the plan and the payment page shows a "Cancel & Return to your store" link at the top. A customer who picked the wrong payment option isn't stranded: clicking it confirms, cancels the unfunded draft, and sends them back to the URL you gave — ideally your checkout with their cart intact. Without it, no link is shown and the customer's only way back is the browser. Strongly recommended for every checkout integration; must be an http(s) URL (anything else is ignored).

There's also a merchant-level fallback: set a Checkout Return URL in the dashboard under Settings and it's used whenever a plan doesn't carry its own checkoutUrl. Handy as a safety net while your integration doesn't send one yet — but the per-order URL is better, because it can point at the customer's actual checkout session.

Prefer email instead of a redirect? Call POST /api/plans/{planId}/send-setup-email with the same headers and FlexiLay emails the customer their setup link.

If the customer doesn't finish setup, the draft expires quietly after ~24 hours — no reminder or cancellation emails are sent for API-created plans, since an abandoned checkout usually means the customer moved on or paid another way. Treat the order as unpaid until you see the deposit confirmed (webhook status active).

4

Track the plan from your site

Push: status webhook (recommended)

In the FlexiLay dashboard under Settings → Status Webhook, enter a public https:// URL on your site. Saving generates a signing secret (shown next to the field). FlexiLay then POSTs to that URL on every plan status change:

POST https://your-site.com/flexilay-webhook
X-Flexilay-Event: plan.status
X-Flexilay-Shop: store.example.com
X-Flexilay-Signature: 3f2a…   // HMAC-SHA256(raw body, signing secret), hex

{
  "event": "plan.status",
  "planId": "ckw…",
  "externalOrderId": "1001",
  "status": "active",
  "currency": "AUD",
  "totalAmountCents": 25000,
  "remainingAmountCents": 20000,
  "sentAt": "2026-08-05T09:30:00.000Z"
}

Always verify the signature before trusting the payload:

// supabase/functions/flexilay-webhook/index.ts — Secret: FLEXILAY_WEBHOOK_SECRET
Deno.serve(async (req) => {
  const raw = await req.text();
  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(Deno.env.get("FLEXILAY_WEBHOOK_SECRET")!),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(raw));
  const expected = [...new Uint8Array(mac)]
    .map((b) => b.toString(16).padStart(2, "0")).join("");

  if (req.headers.get("x-flexilay-signature") !== expected)
    return new Response("bad signature", { status: 401 });

  const { externalOrderId, status, remainingAmountCents } = JSON.parse(raw);
  // e.g. mark order 'paid' when a plan goes active (deposit collected),
  // 'refunded/void' on cancelled, celebrate on completed 🎉
  return new Response("ok");
});

Pull: status API

Poll any time with the same auth headers — useful before fulfilling an order:

GET https://my.flexilay.com/api/plans/{planId}
// → { ok: true, plan: { status, remainingAmount, charges: [...] } }

GET https://my.flexilay.com/api/merchant/plans   // list all your plans
Plan statusMeaning
draftCreated; customer hasn't completed setup yet
scheduled / activeDeposit paid; instalments running — safe to treat the order as secured
completedPaid in full — release / ship the goods (lay-by) if you haven't already
cancelledCancelled by merchant, customer, or after repeated failed payments
abandonedSetup never completed; the draft expired

Quick reference

ActionRequest
Create a planPOST /api/plans/draft
Email setup linkPOST /api/plans/{id}/send-setup-email
Plan status + chargesGET /api/plans/{id}
List plansGET /api/merchant/plans
Cancel a planPOST /api/plans/{id}/cancel
Refund a chargePOST /api/plans/{id}/refund

Base URL https://my.flexilay.com · every request carries X-Flexilay-Shop + X-Flexilay-Api-Key · amounts in integer cents · currency defaults to AUD if omitted · completeBy (YYYY-MM-DD, optional) caps the schedule at a payment deadline · checkoutUrl (optional) adds a "Cancel & return" link to the payment page.

Testing: turn on demo mode in the FlexiLay dashboard (Settings → Test Mode) — you can run the full checkout flow end-to-end without real charges.