Storefront API
Run your own website or app on a FrontDesk merchant's data: their catalogue, events, forms and checkout. You build the frontend. FrontDesk stays the source of truth and handles the money. Base URL https://api.frontdesk.africa/v1. Also available as llms.txt for AI agents, openapi.json for client generation, and lovable.txt to paste into an AI app builder.
Authentication
Send the key as Authorization: Bearer <key>. There are two kinds and the difference is the whole design.
fd_pk_…publishableReads, plus one write: registering a guest for a free event. Safe to ship in browser code. Locked to the exact origins the merchant registered. An empty origin list refuses every call. That is deliberate, not a bug.
fd_sk_…secretCan open checkouts. Server side only. Anyone who can view source can otherwise take payments as that merchant.
Keys carry their environment. fd_pk_test_… works against staging, fd_pk_live_… against production, and a key from the wrong one is refused. You never choose which workspace you are reading. The key decides.
Errors
Success returns the resource itself. Errors look like this:
{ "error": { "code": "...", "message": "...", "retryable": false, "requestId": "..." } }
| Code | Meaning |
|---|---|
UNAUTHORIZED | Missing, unknown, revoked or wrong-environment key. |
ORIGIN_NOT_ALLOWED | Publishable key used from an origin the merchant has not registered. That includes no origin at all. |
INSUFFICIENT_SCOPE | A publishable key tried to do something only a secret key may. |
KEY_PAUSED | A real key the merchant has paused. Ask them to resume it. The value is still good. |
STORE_API_SECRET_DISABLED | Secret keys are switched off for this workspace. The merchant can turn them on under Developers. |
STORE_API_DISABLED | The whole workspace is switched off. |
TIER_REQUIRES_PAYMENT | 402. A free registration asked for a tier that costs money. Open a checkout for it from your server instead. |
TIER_SOLD_OUT | 409. The tier has no capacity left. details.remaining says how many are. |
RATE_LIMITED | 429. Honour Retry-After. |
Rendering a product page
GET /store/products/:slug returns everything a full product page needs. Every block below is optional: render it only when its field is present, and a bare product should show none of this chrome.
Product info — the info object. highlights is the checkmark list under the price. Build a Details table from specs plus brand, modelName, manufacturer, countryOfOrigin and sku. sustainabilityText and careInstructions are their own accordions; longDescription is the prose block and descriptionBlocks renders after it, in order.
Variation groups — the options array, present when the merchant sells along several axes (Size × Colour). Render one picker per group, drawn per that group's own display: dropdown a select, text chips, color swatches from the value's swatchHex, image tiles from the value's media[0]. The buyer picks one value per group, and the chosen set resolves to the one variant whose optionValueRefs holds exactly those refs — send that variant's ref to checkout, unchanged. Dim a value whose every remaining variant is sold out or unpriced in the buyer's currency, and swap the gallery to a value's media when it is picked. When options is absent, use the flat variant picker below.
Variant picker (no options) — switch the UI on variantDisplay: text renders name chips, dropdown renders a single select (for long or numerous names), image renders square swatches from each variant's media[0] (selecting one swaps the gallery to that variant's media), color renders circles from swatchHex, falling back to a text chip when it is null. A sold-out variant stays visible and selectable, just not addable.
The call to action, in this order:
| State | Render |
|---|---|
| In stock | Add to cart, quantity capped on the variant's availableQty (null = no cap). |
preorderable | A pre-order CTA replaces the sold-out state. Cap quantity on preorderRemaining. Show preorderEtaText only when present — never hardcode shipping copy. A set preorderDepositType reads "Pay N% now"; the exact charge is struck server-side on the order total. |
saveForItSaveable | Offer the savings-plan path. |
| Otherwise | Sold out; offer a restock alert when restockAlertsEnabled is true. |
Reviews render only when reviews is present and reviewsEnabled is true — no stars otherwise. Stale carts: recheck on return with GET /store/availability?variantRefs=…; a ref missing from the map is gone (drop the line), a ref listed in preorderable is still buyable at 0 stock (cap on preorderRemaining). On cards, PublicProductSummary carries the pill states (preorderable, saveForItSaveable), the sale badge (compareAtMinorFrom, showSaleBadge) and the star line (ratingAvg/ratingCount).
Taking payment
You never touch card details. Your server opens a checkout with the secret key and an Idempotency-Key, you redirect the buyer to the hostedUrl you get back, and they pay on FrontDesk. They return to your returnUrl with ?checkout=<ref>.
Never treat the return redirect as proof of payment. It is a browser navigation and can be lost, replayed or forged. Confirm with GET /store/checkouts/:ref, or wait for the checkout.completed webhook.
Registering for a free event
A free RSVP needs no server. POST /store/events/:slug/register is the one write a publishable key may make, so your page calls it straight from the browser.
Read the event first: GET /store/events/:slug lists the tiers, and a tier whose kind is free is what you offer. Each tier carries formFields (the questions to ask, keyed by key), groupSize, minPerOrder/maxPerOrder, requiresAttendeeDetails and soldOut; the event carries minAge and dobMode. Then send tickets (the same shape as checkout, with positional attendees to name each seat), contact (a name and an email and/or phone) and responses keyed by each field's key. A plus-one is quantity 2 with the guest named in the second attendees entry.
You get back the guest's reference and status. Show them and tell the guest to check their email: FrontDesk sends the confirmation, the calendar invite and the ticket PDF itself.
Free tiers only. A paid tier answers 402 TIER_REQUIRES_PAYMENT: open a checkout for it from your server. Duplicates are quiet: an email that already holds a seat gets the same 201 as a first registration and its confirmation again (at most every 10 minutes), never a second seat, so a retry is always safe and no Idempotency-Key is needed. To read registrations back, subscribe to the attendee.registered webhook.
Webhooks
The merchant registers up to five destinations, each with its own signing secret and its own event subscriptions, in their FrontDesk portal under Developers. Every delivery is signed:
X-FD-Signature: sha256=HMAC_SHA256(secret, timestamp + "." + rawBody)
X-FD-Timestamp: <unix seconds>
Verify against the raw body with THAT destination's secret, compare in constant time, and reject a timestamp far from now. Deliveries carry X-FD-Webhook-Id and the body's id field — store it with a unique constraint so a retried delivery is processed once. A ping event may arrive when the merchant tests the endpoint; acknowledge it with a 2xx. Payloads carry refs only. Re-fetch through the API rather than trusting figures in the event.
| Event | When | Data |
|---|---|---|
checkout.completed | A hosted checkout was paid. | checkoutRef, orderRef |
checkout.expired | A checkout link lapsed unpaid. | checkoutRef |
order.updated | An order's fulfilment status changed. | orderRef, status, fulfillmentStatus |
form.submitted | A form submission was recorded. | submissionRef, formSlug, formRef |
attendee.registered | A guest registered for a free event: through your own page, the hosted storefront, or the public RSVP link. Never fires for a test key. | attendeeRef, eventRef, eventSlug, ticketTypeRef, status, attendeeCount, groupRef, source |
customer.linked | A buyer account was linked to one of your contacts. | customerRef, contactRef |
product.deleted | A product was deleted from the store. Remove it from any local copy. | productRef, slug |
order.deleted | An order was removed. Today: sandbox retention hard-deleting test orders after 90 days. | orderRef, test, reason |
Sandbox (test mode)
A checkout opened with a test key runs in test mode: the hosted page shows the real rails wired to the provider sandbox, and paying with a provider test card runs the whole loop. The order it creates is real and fetchable, marked test everywhere — it moves no money, credits no wallet, holds no stock or seats, issues no tickets, and never appears in the merchant's live views or sales figures. Rails without a sandbox (bank transfer, Flutterwave) are not offered in test mode. Test orders are retained for 90 days, then hard-deleted.
Supabase backend
If your stack is Supabase you do not have to build the server side yourself. The merchant connects their own Supabase project under Developers → Supabase, and FrontDesk installs Edge Functions holding a dedicated scoped secret key (server-side only — your frontend never sees a key at all), a signed webhook receiver, and a frontdesk schema with realtime checkout rows behind default-deny RLS. Your frontend calls those functions instead of this API directly.
Since v2 the schema also mirrors the catalogue (frontdesk.products) and the merchant's own orders (frontdesk.orders, with a test flag): webhooks keep the mirrors fresh, a scheduled frontdesk-sync function pulls the delta reads for truth, and product.deleted / order.deleted events remove rows. Nothing money-shaped is ever mirrored.
One Supabase project per workspace: installing a second workspace into the same project overwrites the first one's secrets. Disconnecting revokes the key and FrontDesk's access; the tables, functions and data in the project stay exactly as they are.
The credentials rule holds inside your own code too: the browser uses fd_pk_… from an allowed origin; anything server-side (Node, Deno, an Edge Function) uses fd_sk_…. A publishable key called from a server carries no browser origin and is refused.
Storefront
The whole storefront: brand, theme, sections, pages, and the embedded event, collection and product lists
One call is enough to render a home page. Events, collections and products also ride inside this, so you can render everything from one response.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Returns Storefront
curl https://api.frontdesk.africa/v1/store/storefront \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/storefront', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"type": "string",
"displayName": "Matte Lipstick",
"timezone": "Africa/Lagos",
"description": "A long-wear matte lipstick.",
"categorySlug": "string",
"categorySlugs": [
"string"
],
"website": "string",
"socials": {},
"city": "Lagos",
"state": "Lagos",
"country": "Nigeria",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"badges": [
{
"type": "string",
"grantedAt": "2026-08-18T09:20:11.000Z"
}
],
"memberCount": 12,
"renderConfig": {
"templateId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"theme": {
"fontPairing": "string",
"palette": "string",
"brandColor": "string",
"accentSource": "brand",
"spacing": "compact",
"contentWidth": "compact",
"customPalettes": {},
"customFontPacks": {}
},
"sections": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"type": "hero",
"visible": false,
"hasBeenEdited": false,
"options": {}
}
],
"pages": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"type": "portfolio",
"title": "Matte Lipstick",
"slug": "matte-lipstick",
"visible": false,
"metaTitle": "string",
"sections": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"type": "hero",
"visible": false,
"hasBeenEdited": false,
"options": {}
}
],
"options": {}
}
]
},
"content": {
"header": {
"websiteTitle": "string",
"logoText": "string",
"logoImageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"logoMode": "logo_text",
"logoHeight": 1,
"logoSvg": "string",
"logoSvgFill": "string",
"logoSvgStroke": "string",
"logoSvgStrokeWidth": 1,
"faviconUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"socialPreviewStyle": "custom",
"socialCardTemplate": "scrim",
"socialCardHeadline": "string",
"socialCardSubline": "string",
"socialPreviewImageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"layout": "brand_left",
"pillStyle": {
"tone": "auto",
"bgColor": "string",
"textColor": "string",
"radius": 1,
"paddingX": 1,
"paddingY": 1,
"width": "content",
"topOffset": 1,
"sideOffset": 1,
"shadow": "none",
"border": false,
"borderColor": "string",
"blur": false,
"scroll": "stay"
},
"navPosition": "left",
"ctas": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"visible": false,
"fill": "solid",
"bgColor": "string",
"textColor": "string",
"radius": 1,
"fontScale": 1,
"fontFamily": "string",
"fontWeight": 1,
"italic": false,
"underline": false,
"uppercase": false,
"link": {
"type": "none",
"sectionId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"pageSlug": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"packageSlug": "string",
"packageOptionRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"productSlug": "string",
"collectionSlug": "string",
"catalogSlug": "string",
"eventSlug": "open-house"
}
}
],
"linkStyle": {
"color": "string",
"fontScale": 1,
"weight": 1,
"uppercase": false,
"textCase": "auto",
"fontFamily": "string",
"italic": false,
"underline": false
},
"links": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"link": {
"type": "none",
"sectionId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"pageSlug": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"packageSlug": "string",
"packageOptionRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"productSlug": "string",
"collectionSlug": "string",
"catalogSlug": "string",
"eventSlug": "open-house"
},
"visible": false
}
],
"maxWidth": "compact",
"overlayHeader": false,
"overlayColor": "auto",
"overlayLogoImageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"menuShowPhone": false,
"menuShowEmail": false,
"menuShowSocials": false,
"hideCollectionsNav": false,
"palette": "string",
"fontPairing": "string"
},
"hero": {
"eyebrow": "string",
"title": "Matte Lipstick",
"subtitle": "string",
"buttons": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"visible": false,
"fill": "solid",
"bgColor": "string",
"textColor": "string",
"radius": 1,
"fontScale": 1,
"fontFamily": "string",
"fontWeight": 1,
"italic": false,
"underline": false,
"uppercase": false,
"link": {
"type": "none",
"sectionId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"pageSlug": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"packageSlug": "string",
"packageOptionRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"productSlug": "string",
"collectionSlug": "string",
"catalogSlug": "string",
"eventSlug": "open-house"
}
}
],
"backgroundMedia": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"type": "image",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp"
}
],
"backgroundMode": "image",
"backgroundUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"backgroundType": "image",
"overlay": 1,
"slideDuration": 1
},
"heroSections": {},
"fieldStyles": {},
"footer": {
"palette": "string",
"fontPairing": "string",
"template": "minimal",
"brandText": "string",
"tagline": "One night, three stages.",
"copyright": "string",
"statusText": "string",
"showBackToTop": false,
"backgroundColor": "string",
"backgroundImageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"backgroundOverlay": 1,
"logoMode": "logo_text",
"logoImageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"logoSvg": "string",
"logoSvgFill": "string",
"logoSvgStroke": "string",
"logoSvgStrokeWidth": 1,
"logoHeight": 1,
"customColors": false,
"textColor": "string",
"mutedTextColor": "string",
"accentColor": "string",
"copyrightBackgroundColor": "string",
"copyrightTextColor": "string",
"socialLinks": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"platform": "string",
"label": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"visible": false
}
],
"linkGroups": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"title": "Matte Lipstick",
"links": [
{}
]
}
],
"maxWidth": "compact",
"newsletter": {
"enabled": true,
"heading": "string",
"body": "string",
"showNameField": false,
"namePlaceholder": "string",
"emailPlaceholder": "string",
"buttonLabel": "string",
"successMessage": "string",
"consentNote": "string",
"tag": "client",
"welcomeEmailEnabled": false,
"welcomeSubject": "string",
"welcomeBody": "string",
"fieldSize": "small",
"radius": 1,
"backgroundColor": "string",
"textColor": "string",
"accentColor": "string",
"spaceTop": 1,
"spaceBottom": 1,
"showDivider": false,
"dividerOpacity": 1,
"dividerHeight": 1
},
"payments": {
"enabled": true,
"title": "Matte Lipstick",
"backgroundColor": "string",
"textColor": "string",
"accentColor": "string",
"spaceTop": 1,
"spaceBottom": 1
},
"address": {
"enabled": true,
"title": "Matte Lipstick",
"showBusinessName": false,
"showAddress": false,
"showEmail": false,
"showPhone": false,
"backgroundColor": "string",
"textColor": "string",
"accentColor": "string"
},
"hours": {
"enabled": true,
"title": "Matte Lipstick",
"closedLabel": "string",
"backgroundColor": "string",
"textColor": "string",
"accentColor": "string"
},
"trust": {
"enabled": true,
"items": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"icon": "package",
"label": "string",
"emphasis": "string",
"iconColor": "string"
}
],
"backgroundColor": "string",
"textColor": "string",
"accentColor": "string",
"spaceTop": 1,
"spaceBottom": 1,
"showDivider": false,
"dividerOpacity": 1,
"dividerHeight": 1
}
},
"contact": {
"addressLine1": "string",
"addressLine2": "string",
"city": "Lagos",
"state": "Lagos",
"country": "Nigeria",
"googleMapsUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"email": "buyer@example.com",
"phoneNumbers": [
"string"
],
"socials": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"platform": "string",
"label": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"visible": false
}
],
"showMap": false
},
"logoStrips": {},
"faqSections": {},
"imageTextSections": {},
"richTextSections": {},
"blockSections": {},
"mediaGallerySections": {},
"socialSections": {},
"reviewsSections": {},
"profilesSections": {},
"contentShowcaseSections": {},
"newsletterSections": {},
"customHtmlSections": {},
"sectionButtons": {},
"sectionButtonGroups": {},
"tracking": {
"facebookPixelId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"ga4Id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"gtmId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"tiktokPixelId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"snapPixelId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"xPixelId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"xPurchaseEventId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"xAddToCartEventId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"xLeadEventId": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"shareHashedEmail": false,
"customHead": "string",
"customBodyEnd": "string"
},
"promo": {
"campaigns": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"name": "Matte Lipstick",
"enabled": true,
"trigger": "on_load",
"delayMs": 1,
"startAt": "2026-08-18T09:20:11.000Z",
"endAt": "2026-08-18T09:20:11.000Z",
"mediaType": "none",
"imageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"videoUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"videoSoundEnabled": false,
"heading": "string",
"body": "string",
"buttons": [
{}
],
"dismissLabel": "string",
"emailCaptureEnabled": false,
"emailPlaceholder": "string",
"emailButtonLabel": "string",
"emailSuccessMessage": "string",
"emailTag": "client",
"version": 1
}
]
}
},
"catalogs": {},
"assets": {},
"packages": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"type": "string",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"priceMinorFrom": 200000,
"currency": "NGN",
"optionCount": 12
}
],
"packageDetails": {},
"products": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"isPackage": false,
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"currency": "NGN",
"priceMinorFrom": 200000,
"pricesFrom": {},
"compareAtMinorFrom": 200000,
"compareAtFrom": {},
"showSaleBadge": false,
"variantCount": 1,
"soldOut": false,
"saveForItSaveable": false,
"preorderable": false,
"ratingAvg": 4.8,
"ratingCount": 12,
"updatedAt": "2026-08-18T09:20:11.000Z"
}
],
"collections": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"coverKind": "image",
"gallery": [
"string"
],
"productRefs": [
"019f6f85-efbd-75e4-83e4-c2778a318189"
],
"itemCount": 12,
"seoTitle": "string",
"seoDescription": "string"
}
],
"events": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"title": "Matte Lipstick",
"slug": "matte-lipstick",
"tagline": "One night, three stages.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"format": "physical",
"mode": "rsvp",
"startAt": "2026-08-18T09:20:11.000Z",
"endAt": "2026-08-18T09:20:11.000Z",
"publishedAt": "2026-08-18T09:20:11.000Z",
"venueName": "Matte Lipstick",
"city": "Lagos",
"category": "music"
}
],
"primaryDomain": "string",
"verifiedDomains": [
"string"
],
"mediaPolicy": {
"maxDim": 1,
"quality": 1,
"format": "webp"
},
"paymentMethods": [
"string"
],
"businessHours": [
{
"day": "string",
"start": "string",
"end": "string"
}
],
"widgetBubble": {
"key": "string"
},
"reviews": {
"average": 1,
"count": 12,
"items": [
{
"author": "string",
"rating": 1,
"text": "string",
"date": "2026-08-18",
"productName": "Matte Lipstick",
"productSlug": "string"
}
]
},
"forms": [
{
"slug": "matte-lipstick",
"name": "Matte Lipstick",
"description": "A long-wear matte lipstick.",
"submitLabel": "string",
"thankYouMessage": "string",
"fields": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"type": "number",
"required": false,
"helpText": "string",
"placeholder": "string",
"options": {
"choices": [],
"file": {}
},
"identity": "email"
}
]
}
]
}
Products
Every public product, as summaries
Delta reads only see PUBLIC products: a product that was deleted arrives as the product.deleted webhook instead, and one that was merely unpublished simply stops appearing — reconcile against the full list occasionally if you keep a mirror.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Query parameters
updatedSincequeryoptionalISO 8601. Only products changed at or after this moment, ordered oldest change first — for keeping a local copy fresh. Feed the largest updatedAt you received back as the next cursor; the >= comparison re-sends the boundary row, so upsert by ref.
limitqueryoptionalPage size for delta reads, max 200. Without updatedSince the list is the storefront display order, unpaged.
Returns PublicProductSummary[]
curl https://api.frontdesk.africa/v1/store/products \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/products', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
[
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"isPackage": false,
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"currency": "NGN",
"priceMinorFrom": 200000,
"pricesFrom": {},
"compareAtMinorFrom": 200000,
"compareAtFrom": {},
"showSaleBadge": false,
"variantCount": 1,
"soldOut": false,
"saveForItSaveable": false,
"preorderable": false,
"ratingAvg": 4.8,
"ratingCount": 12,
"updatedAt": "2026-08-18T09:20:11.000Z"
}
]
One product with its variants, media, specs and description blocks
When `options` is present the product sells along several axes (Size × Colour): render one picker per group and resolve the buyer's picks to the variant whose `optionValueRefs` holds exactly those value refs. Checkout still takes that variant's ref.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe product's slug.
Returns PublicProduct
curl https://api.frontdesk.africa/v1/store/products/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/products/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"needsDelivery": false,
"currency": "NGN",
"feeMode": "pass_on",
"priceMinorFrom": 200000,
"media": [
"string"
],
"variants": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"priceMinor": 200000,
"compareAtMinor": 200000,
"currency": "NGN",
"prices": [
{
"currency": "NGN",
"priceMinor": 200000,
"compareAtMinor": 200000
}
],
"media": [
"string"
],
"soldOut": false,
"availableQty": 1,
"swatchHex": "string",
"optionValueRefs": [
"019f6f85-efbd-75e4-83e4-c2778a318189"
],
"description": "A long-wear matte lipstick.",
"badge": "string",
"features": [
{
"text": "string",
"included": false
}
],
"fineprint": "string",
"durationMinutes": 1,
"pricingMode": "full_payment",
"schedulingType": "slots",
"availability": {
"weekdays": [
"string"
],
"blocks": [
{
"type": "date",
"date": "2026-08-18"
}
],
"overrideDates": [
"2026-08-18"
],
"windowDays": 1,
"leadTimeMinutes": 1
},
"takenDates": [
"2026-08-18"
],
"formFields": [
{
"key": "string",
"type": "number",
"label": "string",
"required": false,
"hidden": false,
"placeholder": "string",
"helpText": "string",
"options": [
"string"
],
"showIf": {
"field": "string",
"equals": "string"
}
}
]
}
],
"isPackage": false,
"selectionMode": "choose_one",
"variantDisplay": "text",
"options": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"display": "text",
"values": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"label": "string",
"swatchHex": "string",
"media": [
"string"
]
}
]
}
],
"schedulingEnabled": false,
"addonGroups": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"selectionType": "single",
"required": false,
"minSelect": 1,
"maxSelect": 1,
"sort": 1,
"variantRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"options": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"kind": "inline",
"label": "string",
"description": "A long-wear matte lipstick.",
"priceMinor": 200000,
"prices": [
{}
],
"imageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"allowQuantity": false,
"maxQty": 1,
"linkedProductRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"linkedVariantRef": "019f6f85-efbd-75e4-83e4-c2778a318189"
}
]
}
],
"formFields": [
{
"key": "string",
"type": "number",
"label": "string",
"required": false,
"hidden": false,
"placeholder": "string",
"helpText": "string",
"options": [
"string"
],
"showIf": {
"field": "string",
"equals": "string"
}
}
],
"saveForItEnabled": false,
"saveForItMaxDays": 1,
"saveForItFixedDates": [
"2026-08-18"
],
"saveForItFreezePrice": false,
"saveForItCrowdLabel": "string",
"saveForItSaveable": false,
"preorderable": false,
"preorderEtaText": "string",
"preorderRemaining": 1,
"preorderDepositType": "percent",
"preorderDepositValue": 1,
"soldOut": false,
"reviews": {
"count": 12,
"average": 1,
"counts": {
"1": 1,
"2": 1,
"3": 1,
"4": 1,
"5": 1
},
"aiSummary": "string"
},
"reviewsEnabled": false,
"restockAlertsEnabled": false,
"info": {
"brand": "string",
"infoCategory": "home",
"longDescription": "string",
"descriptionBlocks": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"kind": "text",
"text": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"platform": "instagram",
"alt": null,
"caption": "string",
"width": "full",
"sort": 1
}
],
"highlights": [
"string"
],
"sku": "MTL-001",
"barcode": "string",
"mpn": "string",
"manufacturer": "string",
"modelName": "Matte Lipstick",
"weightValue": 1,
"weightUnit": "kg",
"dimLength": 1,
"dimWidth": 1,
"dimHeight": 1,
"dimUnit": "cm",
"countryOfOrigin": "string",
"warrantyText": "string",
"warrantyMonths": 1,
"careInstructions": "string",
"sustainabilityText": "string",
"specs": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"key": "string",
"label": "string",
"value": "string",
"kind": "number",
"unit": "string",
"sort": 1
}
]
}
}
Bookable slots for a product that is scheduled rather than shipped
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe product's slug.
Returns PublicProductSlots
curl https://api.frontdesk.africa/v1/store/products/matte-lipstick/slots \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/products/matte-lipstick/slots', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"date": "2026-08-18",
"slots": [
{
"startUtc": "string",
"startLocal": "string"
}
]
}
Live stock and slot availability
A cart lives in the buyer's storage, so its lines can be days old. Call this when the buyer returns: `variants` maps each ref to units left (null = untracked, no cap), and refs listed in `preorderable` are still buyable at 0 stock — cap those on `preorderRemaining` instead of dropping them.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Query parameters
variantRefsqueryrequiredComma-separated variant refs your cart holds (max 100). A ref missing from the response map is no longer purchasable, so drop that line.
Returns PublicStoreAvailability
curl https://api.frontdesk.africa/v1/store/availability \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/availability', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"variants": {},
"preorderable": [
"string"
],
"preorderRemaining": {}
}
Collections
Every public collection, as summaries
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Returns PublicCollectionSummary[]
curl https://api.frontdesk.africa/v1/store/collections \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/collections', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
[
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"coverKind": "image",
"gallery": [
"string"
],
"productRefs": [
"019f6f85-efbd-75e4-83e4-c2778a318189"
],
"itemCount": 12,
"seoTitle": "string",
"seoDescription": "string"
}
]
One collection with its products
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe collection's slug.
Returns PublicCollectionDetail
curl https://api.frontdesk.africa/v1/store/collections/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/collections/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"collection": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"coverKind": "image",
"gallery": [
"string"
],
"productRefs": [
"019f6f85-efbd-75e4-83e4-c2778a318189"
],
"itemCount": 12,
"seoTitle": "string",
"seoDescription": "string"
},
"products": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"isPackage": false,
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"currency": "NGN",
"priceMinorFrom": 200000,
"pricesFrom": {},
"compareAtMinorFrom": 200000,
"compareAtFrom": {},
"showSaleBadge": false,
"variantCount": 1,
"soldOut": false,
"saveForItSaveable": false,
"preorderable": false,
"ratingAvg": 4.8,
"ratingCount": 12,
"updatedAt": "2026-08-18T09:20:11.000Z"
}
]
}
Events
Every public event, as summaries
Same list that rides inside /v1/store/storefront. Use this when you only need the events.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Returns PublicEventSummary[]
curl https://api.frontdesk.africa/v1/store/events \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/events', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
[
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"title": "Matte Lipstick",
"slug": "matte-lipstick",
"tagline": "One night, three stages.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"format": "physical",
"mode": "rsvp",
"startAt": "2026-08-18T09:20:11.000Z",
"endAt": "2026-08-18T09:20:11.000Z",
"publishedAt": "2026-08-18T09:20:11.000Z",
"venueName": "Matte Lipstick",
"city": "Lagos",
"category": "music"
}
]
One event with its ticket types, days and location
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe event's slug.
Returns PublicEvent
curl https://api.frontdesk.africa/v1/store/events/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/events/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"title": "Matte Lipstick",
"slug": "matte-lipstick",
"tagline": "One night, three stages.",
"description": "A long-wear matte lipstick.",
"mode": "rsvp",
"format": "physical",
"location": {
"venueName": "Matte Lipstick",
"address": "string",
"city": "Lagos",
"state": "Lagos",
"country": "Nigeria",
"lat": 1,
"lng": 3.4881,
"onlineUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"onlineProvider": "string"
},
"venueVisibility": "public",
"timezone": "Africa/Lagos",
"startAt": "2026-08-18T09:20:11.000Z",
"endAt": "2026-08-18T09:20:11.000Z",
"doorsAt": "2026-08-18T09:20:11.000Z",
"capacity": 1,
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"media": [
{
"type": "image",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"thumbnail": "string"
}
],
"category": "music",
"minAge": 1,
"dobMode": "order",
"attendeeEmailRequired": false,
"frontdeskFee": {
"pct": 1,
"flatMinor": 200000
},
"customerFee": {
"pct": 1,
"flatMinor": 200000
},
"customerFeeFlatByCurrency": {},
"days": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"label": "string",
"dayDate": "2026-08-18",
"startAt": "2026-08-18T09:20:11.000Z",
"endAt": "2026-08-18T09:20:11.000Z",
"timezone": "Africa/Lagos",
"location": {
"venueName": "Matte Lipstick",
"address": "string",
"city": "Lagos",
"state": "Lagos",
"country": "Nigeria",
"lat": 1,
"lng": 3.4881,
"onlineUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"onlineProvider": "string"
},
"capacity": 1,
"remaining": 1,
"soldOut": false
}
],
"saveForItEnabled": false,
"saveForItCrowdLabel": "string",
"ticketTypes": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"description": "A long-wear matte lipstick.",
"imageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"kind": "deposit",
"frontdeskFeeMode": "pass_on",
"priceMinor": 200000,
"currency": "NGN",
"prices": [
{
"currency": "NGN",
"priceMinor": 200000,
"compareAtMinor": 200000
}
],
"compareAtMinor": 200000,
"minPriceMinor": 200000,
"donationPresets": [
1
],
"depositType": "percent",
"depositValue": 1,
"depositDueNowMinor": 200000,
"depositBalanceMode": "online",
"depositBalanceDueAt": "2026-08-18T09:20:11.000Z",
"priceEndsAt": "2026-08-18T09:20:11.000Z",
"priceRemaining": 1,
"capacity": 1,
"soldCount": 12,
"remaining": 1,
"soldOut": false,
"lowStockThreshold": 1,
"minPerOrder": 1,
"maxPerOrder": 1,
"minAge": 1,
"approvalRequired": false,
"groupSize": 1,
"validDays": [
"string"
],
"requiresAttendeeDetails": false,
"formFields": [
{
"key": "string",
"type": "number",
"label": "string",
"required": false,
"hidden": false,
"placeholder": "string",
"helpText": "string",
"options": [
"string"
],
"showIf": {
"field": "string",
"equals": "string"
}
}
],
"saveForIt": {
"enabled": true,
"holdPolicy": "none",
"holdThresholdPct": 1,
"priceMode": "freeze",
"cutoffHours": 1
},
"saveForItSaveable": false
}
],
"attachedPackages": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"type": "string",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"currency": "NGN",
"options": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"pricingMode": "full_payment",
"priceMinor": 200000,
"compareAtMinor": 200000,
"currency": "NGN",
"badge": "string",
"media": [
"string"
],
"features": [
"string"
],
"fineprint": "string",
"occupancyMode": "exclusive",
"schedulingType": "slots",
"availability": {
"weekdays": [],
"blocks": [],
"overrideDates": [],
"windowDays": 1,
"leadTimeMinutes": 1
},
"addonGroups": [
{}
],
"formFields": [
{}
],
"takenDates": [
"2026-08-18"
]
}
]
}
],
"attachedProducts": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"needsDelivery": false,
"currency": "NGN",
"feeMode": "pass_on",
"priceMinorFrom": 200000,
"media": [
"string"
],
"variants": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"priceMinor": 200000,
"compareAtMinor": 200000,
"currency": "NGN",
"prices": [
{}
],
"media": [
"string"
],
"soldOut": false,
"availableQty": 1,
"swatchHex": "string",
"optionValueRefs": [
"019f6f85-efbd-75e4-83e4-c2778a318189"
],
"description": "A long-wear matte lipstick.",
"badge": "string",
"features": [
{}
],
"fineprint": "string",
"durationMinutes": 1,
"pricingMode": "full_payment",
"schedulingType": "slots",
"availability": {
"weekdays": [],
"blocks": [],
"overrideDates": [],
"windowDays": 1,
"leadTimeMinutes": 1
},
"takenDates": [
"2026-08-18"
],
"formFields": [
{}
]
}
],
"isPackage": false,
"selectionMode": "choose_one",
"variantDisplay": "text",
"options": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"display": "text",
"values": [
{}
]
}
],
"schedulingEnabled": false,
"addonGroups": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"selectionType": "single",
"required": false,
"minSelect": 1,
"maxSelect": 1,
"sort": 1,
"variantRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"options": [
{}
]
}
],
"formFields": [
{
"key": "string",
"type": "number",
"label": "string",
"required": false,
"hidden": false,
"placeholder": "string",
"helpText": "string",
"options": [
"string"
],
"showIf": {
"field": "string",
"equals": "string"
}
}
],
"saveForItEnabled": false,
"saveForItMaxDays": 1,
"saveForItFixedDates": [
"2026-08-18"
],
"saveForItFreezePrice": false,
"saveForItCrowdLabel": "string",
"saveForItSaveable": false,
"preorderable": false,
"preorderEtaText": "string",
"preorderRemaining": 1,
"preorderDepositType": "percent",
"preorderDepositValue": 1,
"soldOut": false,
"reviews": {
"count": 12,
"average": 1,
"counts": {
"1": 1,
"2": 1,
"3": 1,
"4": 1,
"5": 1
},
"aiSummary": "string"
},
"reviewsEnabled": false,
"restockAlertsEnabled": false,
"info": {
"brand": "string",
"infoCategory": "home",
"longDescription": "string",
"descriptionBlocks": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"kind": "text",
"text": "string",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"platform": "instagram",
"alt": null,
"caption": "string",
"width": "full",
"sort": 1
}
],
"highlights": [
"string"
],
"sku": "MTL-001",
"barcode": "string",
"mpn": "string",
"manufacturer": "string",
"modelName": "Matte Lipstick",
"weightValue": 1,
"weightUnit": "kg",
"dimLength": 1,
"dimWidth": 1,
"dimHeight": 1,
"dimUnit": "cm",
"countryOfOrigin": "string",
"warrantyText": "string",
"warrantyMonths": 1,
"careInstructions": "string",
"sustainabilityText": "string",
"specs": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"key": "string",
"label": "string",
"value": "string",
"kind": "number",
"unit": "string",
"sort": 1
}
]
}
}
],
"brand": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"displayName": "Matte Lipstick",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"handle": "beauty-pro-1"
},
"storefrontPublished": false,
"sponsorship": {
"slotType": "official_ticket_sponsor",
"brandName": "Matte Lipstick",
"logoUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"tagline": "One night, three stages.",
"clickUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"sponsorshipRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"qrColor": "string",
"bannerUrl": "https://cdn.frontdesk.africa/media/lipstick.webp"
}
}
Open a hosted checkout for event tickets
Send Idempotency-Key: a retry returns the SAME checkout instead of two. Tiers are validated here, like product variants: a ref that is unknown, hidden without its unlockCode, sold out, or asked for outside its minPerOrder/maxPerOrder returns 400 VALIDATION_ERROR with the refs in details.invalid, details.soldOut or details.quantity, so the failure lands on your server and never on your buyer. You collect what the organiser asks for, and GET /v1/store/events/:slug tells you what that is: each tier carries formFields, requiresAttendeeDetails, groupSize and minPerOrder/maxPerOrder, and the event carries minAge and dobMode. Everything you send is re-validated here, so prices, availability and age limits are decided by the server, not by your form. Three of those rules REFUSE the order rather than warn, so collect them before you send: a required field left unanswered returns 400 "Please answer: <label>"; a tier with requiresAttendeeDetails returns 400 "Enter a name for each \"<tier>\" ticket" unless every seat carries a name; and an age-gated tier fails the age check without a date of birth, on the buyer when dobMode is order and on each attendee when it is attendee. Our own hosted checkout collects all three for you, and lets the buyer name each guest even when the tier does not demand it, so this only applies when you build your own form.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Path parameters
slugpathrequiredThe event's slug.
Body parameters
ticketsbodyrequiredTicket lines: ticketTypeRef, quantity, optional amountMinor for donation tiers, and attendees to issue seats to named guests. attendees is POSITIONAL and its length must be quantity x groupSize: leave an entry blank to keep that seat under the buyer, never drop it, or the following names shift onto the wrong seats.
productsbodyoptionalNOT SUPPORTED YET: sending a non-empty array returns 400. Sell merchandise with a separate POST /v1/store/checkouts.
contactbodyrequiredBuyer name, email and optional phone. The email receives the tickets.
dateOfBirthbodyoptionalBuyer DOB as YYYY-MM-DD. Required when a chosen tier sets minAge and the event dobMode is order.
responsesbodyoptionalAnswers to the questions on the chosen tiers, keyed by the field key. Required whenever one of those fields is required.
discountCodebodyoptionalPromo code. Re-validated server-side.
unlockCodesbodyoptionalCodes that reveal hidden or invite-only tiers. Re-verified server-side, and REQUIRED here for such a tier: without the code the tier is not on sale and the request is refused.
promoterCodebodyoptionalAffiliate code from a share link. Ignored if unknown.
currencybodyoptionalMust be a currency the event sells in. Omit for its primary.
returnUrlbodyrequiredWhere the buyer lands after paying.
Returns StoreCheckoutSessionView
curl -X POST https://api.frontdesk.africa/v1/store/events/matte-lipstick/checkout \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"tickets": "value",
"products": "optional",
"contact": "value",
"dateOfBirth": "optional",
"responses": "optional",
"discountCode": "optional",
"unlockCodes": "optional",
"promoterCode": "optional",
"currency": "optional",
"returnUrl": "value"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/events/matte-lipstick/checkout', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"tickets": "value",
"products": "optional",
"contact": "value",
"dateOfBirth": "optional",
"responses": "optional",
"discountCode": "optional",
"unlockCodes": "optional",
"promoterCode": "optional",
"currency": "optional",
"returnUrl": "value"
}),
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "open",
"mode": "live",
"hostedUrl": "https://beauty-pro-1.sme.pro/checkout/01a00e95-873c-7056-b31a-aad4c70191e0",
"returnUrl": "https://yourbrand.com/thanks",
"currency": "NGN",
"orderRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"expiresAt": "2026-08-18T09:20:11.000Z",
"completedAt": "2026-08-18T09:20:11.000Z",
"createdAt": "2026-08-18T09:20:11.000Z"
}
Register a guest for a FREE event tier from your own page
FrontDesk sends the confirmation, the calendar invite and the ticket.
The one write a publishable key may make, so call it straight from the browser, from an origin registered on the key. FREE tiers only: a paid, donation or deposit tier answers 402 TIER_REQUIRES_PAYMENT, and that is your cue to open POST /v1/store/events/:slug/checkout from your server instead. Every rule the hosted storefront applies runs here too: an unknown, hidden-without-its-code, or not-yet-on-sale tier is refused, capacity answers 409 TIER_SOLD_OUT, minPerOrder/maxPerOrder are enforced, a required question left unanswered returns 400 "Please answer: <label>", a tier with requiresAttendeeDetails needs a name on every seat, and an age gate needs the date of birth. Duplicates are quiet: when the email already holds a seat on this event you get the SAME 201 as a first registration (their existing reference and status), the confirmation is re-sent at most once every 10 minutes, and no new seat is issued, so a retry is always safe when an email is given and no Idempotency-Key is needed. A test key runs every one of those checks and answers test:true with a test_ reference, writing nothing and sending nothing. There is no read-back endpoint: subscribe to the attendee.registered webhook, and the organiser sees every guest in their portal.
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe event's slug.
Body parameters
ticketsbodyrequiredSame shape as checkout: ticketTypeRef, quantity, and optional attendees to name each seat. attendees is POSITIONAL and its length must be quantity x groupSize: leave an entry blank ({}) to keep that seat under the registrant, never drop it. Every tier must be free.
contactbodyrequiredThe registrant: name, and an email and/or phone. The email receives the confirmation, the .ics invite and the ticket PDF; a phone-only guest gets an SMS.
dateOfBirthbodyoptionalYYYY-MM-DD. Required when a chosen tier sets minAge and the event dobMode is order; per attendee when it is attendee.
responsesbodyoptionalAnswers to the chosen tiers' questions, keyed by each formFields[].key from GET /v1/store/events/:slug. Required whenever one of those fields is required.
unlockCodesbodyoptionalCodes that reveal hidden or invite-only tiers. Required for such a tier.
promoterCodebodyoptionalAffiliate code from a share link. Ignored if unknown.
Returns StoreEventRegistrationView
curl -X POST https://api.frontdesk.africa/v1/store/events/matte-lipstick/register \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com" \
-H "Content-Type: application/json" \
-d '{
"tickets": [
{
"ticketTypeRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"quantity": 2,
"attendees": [
{},
{
"name": "Tunde Obi",
"email": "tunde@example.com"
}
]
}
],
"contact": {
"name": "Ada Obi",
"email": "ada@example.com",
"phone": "+2348120700080"
},
"responses": {
"piggyvest_user": "Yes",
"first_time": "No"
}
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/events/matte-lipstick/register', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"tickets": [
{
"ticketTypeRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"quantity": 2,
"attendees": [
{},
{
"name": "Tunde Obi",
"email": "tunde@example.com"
}
]
}
],
"contact": {
"name": "Ada Obi",
"email": "ada@example.com",
"phone": "+2348120700080"
},
"responses": {
"piggyvest_user": "Yes",
"first_time": "No"
}
}),
});
const data = await res.json();
{
"eventSlug": "open-house",
"eventRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"ticketTypeRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "confirmed",
"reference": "EV-7F3A2C",
"attendeeCount": 1,
"test": false,
"message": "Human readable detail about what happened."
}
Open a ticket checkout you render yourself, including the payment step
Seats are held until expiresAt. Name ONE rail: minting every rail at once would leave several live payment paths on one order, and a buyer could pay twice. Branch on the provider you asked for, never on which payment field is filled in: Paystack returns an accessCode AND a checkoutUrl, so code that redirects whenever checkoutUrl is present sends every card buyer to a Paystack page and never opens the popup. Paystack returns an accessCode for its inline popup: load js.paystack.co/v2/inline.js and call new PaystackPop().resumeTransaction({ accessCode, onSuccess, onCancel }). Pass ONE options object, because resumeTransaction takes a single argument and the older-looking resumeTransaction(accessCode, callbacks) silently falls back to a full-page redirect to Paystack. Stripe returns a checkoutUrl to send the buyer to. Bank transfer returns an account once the provider confirms it, so poll GET /v1/store/checkouts/:ref if it is null. Requires headless to be switched on for the workspace. Release the seats early with POST /v1/store/checkouts/:ref/cancel when the buyer walks away. Every rail returns the buyer to your returnUrl with ?checkout=<ref> appended, so they never see a FrontDesk page. Treat that redirect as a hint and confirm with GET /v1/store/checkouts/:ref. With a TEST key this creates a REAL order marked test (no money, no stock, no tickets) and charges against the provider sandbox — pay it with a provider test card. Where sandbox credentials are not wired, it validates and prices only: payment fields are null and orderRef is a synthetic test_ord_ value that resolves to nothing.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Path parameters
slugpathrequiredThe event's slug.
Body parameters
providerbodyrequiredThe rail to use, from GET /v1/store/payment-methods.
ticketsbodyrequiredTicket lines: ticketTypeRef, quantity, optional amountMinor for donation tiers.
contactbodyrequiredBuyer name, email and optional phone.
pricingbodyoptionalYour computed totalMinor. We re-price and return PRICE_MISMATCH if it disagrees.
returnUrlbodyrequiredYour own page, where the buyer lands after paying. Must be https, or localhost while you build.
Returns StoreHeadlessCheckoutView
curl -X POST https://api.frontdesk.africa/v1/store/events/matte-lipstick/checkout/headless \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"provider": "value",
"tickets": "value",
"contact": "value",
"pricing": "optional",
"returnUrl": "value"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/events/matte-lipstick/checkout/headless', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"provider": "value",
"tickets": "value",
"contact": "value",
"pricing": "optional",
"returnUrl": "value"
}),
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"orderRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "open",
"mode": "live",
"amount": 200000,
"provider": "paystack",
"payment": {
"accessCode": "string",
"checkoutUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"account": {
"accountNumber": "string",
"accountName": "Matte Lipstick",
"bankName": "Matte Lipstick"
},
"reference": "EV-7F3A2C"
},
"expiresAt": "2026-08-18T09:20:11.000Z"
}
Content
A published form and its field schema, so you can render it yourself
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe form's slug.
Returns PublicForm
curl https://api.frontdesk.africa/v1/store/forms/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/forms/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"slug": "matte-lipstick",
"name": "Matte Lipstick",
"description": "A long-wear matte lipstick.",
"status": "draft",
"fields": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"type": "number",
"required": false,
"helpText": "string",
"placeholder": "string",
"options": {
"choices": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"color": "string"
}
],
"file": {
"minFiles": 1,
"maxFiles": 1,
"kinds": "image"
}
},
"identity": "email"
}
],
"settings": {
"displayTitle": "string",
"displayDescription": "string",
"sideTitle": "string",
"sideDescription": "string",
"thankYouMessage": "string",
"redirectUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"submitButtonLabel": "string"
},
"coverUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"workspace": {
"name": "Matte Lipstick",
"handle": "beauty-pro-1",
"logoUrl": "https://cdn.frontdesk.africa/media/lipstick.webp"
},
"uploadToken": "string"
}
A published data table: its columns and rows
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe table's slug.
Returns PublicTable
curl https://api.frontdesk.africa/v1/store/tables/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/tables/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"name": "Matte Lipstick",
"description": "A long-wear matte lipstick.",
"icon": "string",
"slug": "matte-lipstick",
"publicPath": "string",
"columns": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"name": "Matte Lipstick",
"type": "string",
"options": {
"choices": [
{
"id": "01a00e97-62c6-73cc-9fa2-7ba986691c25",
"label": "string",
"color": "string"
}
],
"currency": "NGN",
"agg": "string"
},
"width": 1
}
],
"rows": [
{}
],
"total": 1,
"truncated": false,
"aggregates": {},
"updatedAt": "2026-08-18T09:20:11.000Z",
"lastEditedAt": "2026-08-18T09:20:11.000Z",
"brand": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"displayName": "Matte Lipstick",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"handle": "beauty-pro-1"
},
"storefrontPublished": false
}
A public media catalog (a gallery)
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe catalog's slug.
Returns PublicCatalog
curl https://api.frontdesk.africa/v1/store/catalogs/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/catalogs/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"catalog": {
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"allowDownloads": false,
"fullScreenMasonry": false,
"showGalleryName": false,
"showGalleryDescription": false,
"seoIndexable": false,
"type": "standard",
"brief": {
"theme": "string",
"palette": [
"string"
],
"occasion": "string",
"notes": "string"
}
},
"items": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"type": "image",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"thumbnailUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"title": "Matte Lipstick",
"description": "A long-wear matte lipstick.",
"linkUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"tags": [
"string"
],
"width": 1,
"height": 1
}
],
"brand": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"displayName": "Matte Lipstick",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp"
},
"storefrontPublished": false
}
A public file folder and its files
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe folder's slug.
Returns PublicFolder
curl https://api.frontdesk.africa/v1/store/files/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/files/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"folder": {
"name": "Matte Lipstick",
"slug": "matte-lipstick",
"description": "A long-wear matte lipstick.",
"allowDownloads": false,
"seoIndexable": false
},
"files": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"fileKind": "image",
"url": "https://cdn.frontdesk.africa/media/lipstick.webp",
"thumbnailUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"filename": "Matte Lipstick",
"mimeType": "string",
"sizeBytes": 1,
"width": 1,
"height": 1,
"createdAt": "2026-08-18T09:20:11.000Z"
}
],
"brand": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"displayName": "Matte Lipstick",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp"
},
"storefrontPublished": false
}
A published note or document
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Path parameters
slugpathrequiredThe note's slug.
Returns PublicNote
curl https://api.frontdesk.africa/v1/store/notes/matte-lipstick \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/notes/matte-lipstick', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"title": "Matte Lipstick",
"slug": "matte-lipstick",
"contentHtml": "string",
"publicPath": "string",
"updatedAt": "2026-08-18T09:20:11.000Z",
"lastEditedAt": "2026-08-18T09:20:11.000Z",
"approvals": [
{
"approverName": "Matte Lipstick",
"approverEmail": "buyer@example.com",
"note": "string",
"approvedAt": "2026-08-18T09:20:11.000Z"
}
],
"approvalCount": 12,
"comments": [
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"authorKind": "client",
"authorName": "Matte Lipstick",
"body": "string",
"createdAt": "2026-08-18T09:20:11.000Z"
}
],
"brand": {
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"displayName": "Matte Lipstick",
"avatarUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"handle": "beauty-pro-1"
},
"storefrontPublished": false
}
Delivery
Delivery zones and their fees, so a cart can quote shipping before checkout
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Returns DeliveryZone[]
curl https://api.frontdesk.africa/v1/store/delivery-zones \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/delivery-zones', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
[
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"name": "Matte Lipstick",
"feeMinor": 200000,
"currency": "NGN",
"sort": 1,
"prices": [
{
"currency": "NGN",
"feeMinor": 200000
}
]
}
]
Checkout
Open a hosted checkout
Returns a URL to send the buyer to; you never handle payment.
Requires an Idempotency-Key header. A retry then returns the SAME session instead of giving the buyer two carts. Items are validated here: an unknown or unpublished variantRef returns 400 VALIDATION_ERROR with the bad refs in `details.invalid`, and a variant with no price in the requested currency returns 400 VALIDATION_ERROR with the refs in `details.unpriced`, so both fail on your server rather than in front of your buyer. Pricing still happens when the buyer pays, in the session currency. Only rails that can charge that currency are offered on the hosted page (NGN: card, bank transfer; anything else: international card). A buyer who backs out of a payment returns to the hosted page, never to your returnUrl: only a paid checkout reaches returnUrl. A retry there pays the same order rather than opening another, and reopening hostedUrl resumes it, including a bank transfer already started. There is no `provider` field: the buyer picks how to pay on our page, which is why you never touch card data.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Idempotency-KeystringrequiredYour own reference for this attempt. Sending it again returns the same checkout instead of a second one.
Body parameters
itemsbodyrequiredArray of { variantRef, quantity }. Every variantRef must resolve to a live product in this store; refs come from GET /v1/store/products.
returnUrlbodyrequiredWhere the buyer lands after paying. https, or localhost while testing.
contactbodyoptionalPrefills the hosted page. The buyer can still correct it.
currencybodyoptionalThe currency the buyer pays in. Must be one the store accepts, and every item must carry a price in it (see prices[] on GET /v1/store/products); otherwise 400 VALIDATION_ERROR with `details.accepted` or `details.unpriced`. Omit for the store primary. Fixed for the life of the session: the hosted page, the order and the payment are all in it, and nothing falls back to the store primary.
deliveryZoneRefbodyoptionalRequired when any line needs delivery. The zone fee is charged in the checkout currency.
Returns StoreCheckoutSessionView
curl -X POST https://api.frontdesk.africa/v1/store/checkouts \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Idempotency-Key: order-1042" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"variantRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"quantity": 1
}
],
"contact": {
"name": "Ada Obi",
"email": "ada@example.com"
},
"returnUrl": "https://yourbrand.com/thanks"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/checkouts', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Idempotency-Key': 'order-1042',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"items": [
{
"variantRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"quantity": 1
}
],
"contact": {
"name": "Ada Obi",
"email": "ada@example.com"
},
"returnUrl": "https://yourbrand.com/thanks"
}),
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "open",
"mode": "live",
"hostedUrl": "https://beauty-pro-1.sme.pro/checkout/01a00e95-873c-7056-b31a-aad4c70191e0",
"returnUrl": "https://yourbrand.com/thanks",
"currency": "NGN",
"orderRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"expiresAt": "2026-08-18T09:20:11.000Z",
"completedAt": "2026-08-18T09:20:11.000Z",
"createdAt": "2026-08-18T09:20:11.000Z"
}
The state of a checkout, including the order ref once it is paid
THIS is how you confirm a purchase. The return redirect is a browser navigation and can be lost, replayed or forged. Never fulfil on it alone. Safe to poll while a buyer is paying: when the checkout is still open we ask the payment provider directly before answering, so you get a real answer even if a webhook is late or never arrives. Polling every few seconds is fine and costs you nothing extra.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Path parameters
refpathrequiredThe checkout ref.
Returns StoreCheckoutSessionView
curl https://api.frontdesk.africa/v1/store/checkouts/REF \
-H "Authorization: Bearer fd_sk_live_xxx"
const res = await fetch('https://api.frontdesk.africa/v1/store/checkouts/REF', {
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "open",
"mode": "live",
"hostedUrl": "https://beauty-pro-1.sme.pro/checkout/01a00e95-873c-7056-b31a-aad4c70191e0",
"returnUrl": "https://yourbrand.com/thanks",
"currency": "NGN",
"orderRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"expiresAt": "2026-08-18T09:20:11.000Z",
"completedAt": "2026-08-18T09:20:11.000Z",
"createdAt": "2026-08-18T09:20:11.000Z"
}
Cancel an open checkout and release the seats it was holding
Call this when your buyer abandons the payment step. A headless ticket checkout holds its tiers against capacity until expiresAt, so cancelling puts them back on sale straight away instead of leaving them out of stock for the rest of the window. Cancelling a checkout that is already paid returns 409, and a checkout that lapses on its own is released by us.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Path parameters
refpathrequiredThe checkout ref.
Returns StoreCheckoutSessionView
curl -X POST https://api.frontdesk.africa/v1/store/checkouts/REF/cancel \
-H "Authorization: Bearer fd_sk_live_xxx"
const res = await fetch('https://api.frontdesk.africa/v1/store/checkouts/REF/cancel', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
},
});
const data = await res.json();
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"status": "open",
"mode": "live",
"hostedUrl": "https://beauty-pro-1.sme.pro/checkout/01a00e95-873c-7056-b31a-aad4c70191e0",
"returnUrl": "https://yourbrand.com/thanks",
"currency": "NGN",
"orderRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"expiresAt": "2026-08-18T09:20:11.000Z",
"completedAt": "2026-08-18T09:20:11.000Z",
"createdAt": "2026-08-18T09:20:11.000Z"
}
Orders
Your own store orders, newest change last — built for keeping a local copy (a Supabase mirror, a dashboard) in sync
The key's env picks the set: a live key never sees test orders and a test key never sees live ones, and `test` is also on every row. In-progress savings plans (`saving`) never appear — they are not orders yet. A page shorter than `limit` means you are caught up. Deletions arrive as the order.deleted webhook (today that is sandbox retention removing 90-day-old test orders). This is a sync and back-office read: to confirm a single purchase, keep using GET /v1/store/checkouts/:ref.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Query parameters
updatedSincequeryoptionalISO 8601. Only orders changed at or after this moment, ordered by updatedAt asc. Feed the largest updatedAt you received back as the next cursor; the >= comparison re-sends the boundary row, so upsert by ref.
limitqueryoptionalPage size, 1 to 200. Defaults to 100.
Returns StoreOrderRecord[]
curl https://api.frontdesk.africa/v1/store/orders \
-H "Authorization: Bearer fd_sk_live_xxx"
const res = await fetch('https://api.frontdesk.africa/v1/store/orders', {
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
},
});
const data = await res.json();
[
{
"ref": "019f6f85-efbd-75e4-83e4-c2778a318189",
"shortReference": "string",
"status": "paid",
"fulfillmentStatus": "fulfilled",
"test": false,
"currency": "NGN",
"subtotalMinor": 200000,
"deliveryFeeMinor": 200000,
"totalMinor": 200000,
"buyer": {
"name": "Matte Lipstick",
"email": "buyer@example.com",
"phone": "+2348120700080"
},
"needsDelivery": false,
"shipping": {
"address1": "string",
"address2": "string",
"city": "Lagos",
"state": "Lagos",
"country": "Nigeria",
"zoneRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"zoneName": "Matte Lipstick"
},
"lines": [
{
"productRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"productName": "Matte Lipstick",
"variantRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"variantName": "Matte Lipstick",
"imageUrl": "https://cdn.frontdesk.africa/media/lipstick.webp",
"quantity": 1,
"unitPriceMinor": 200000,
"subtotalMinor": 200000
}
],
"paidAt": "2026-08-18T09:20:11.000Z",
"createdAt": "2026-08-18T09:20:11.000Z",
"updatedAt": "2026-08-18T09:20:11.000Z"
}
]
One-time passwords
Send a one-time password to your own customer over WhatsApp, from your own connected WhatsApp number
This costs money on every send, whether or not your customer ever reads it: an authentication template is business-initiated, so the free 24-hour reply window does not apply. The response quotes what was charged. WhatsApp OTP in Nigeria is several times the price of SMS — the reasons to choose it are deliverability and one-tap copy, not cost, and an international destination costs roughly 11x the Nigerian rate. THE CODE IS NEVER RETURNED, even when we minted it: hold the challengeRef and call /v1/store/otp/verify. `to` comes back masked. Before this works the workspace needs a connected WhatsApp number and an approved authentication template (Set up OTP in the portal), and the destination's country on its allow-list, which starts as the workspace's own country only. Throttled per destination independently of this key's rate limit: 60 seconds between codes to one number, 5 an hour, 10 a day. Each KEY also has its own ceiling on total sends per rolling 24 hours (100 by default, settable per key in the portal), so one leaked credential cannot spend the workspace's whole daily WhatsApp allowance and take your other keys down with it. Nothing is charged when a guard refuses. WITH `sender: 'frontdesk'` none of the setup applies — no number to connect, no business verification, no template — but the code identifies FrontDesk rather than you, and the price is exactly the same. If we cannot send from our number it returns 503 OTP_SENDER_UNAVAILABLE and charges nothing; it never quietly falls back to yours, because a code from an unexpected number that you paid for is worse than a refusal you can read.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Body parameters
tobodyrequiredYour customer's number in full international format, e.g. +2348012345678.
codebodyoptionalYour own code, digits only, up to 15 characters. Omit it and we mint a 6-digit one — which is also the ONLY way /v1/store/otp/verify can work, because a code you chose is one we never see in the clear.
purposebodyoptionalYour own label for what this code was for ("login", "payout"). Stored for your send log, never shown to the customer.
expiresInMinutesbodyoptionalHow long the code stays valid. Default 10, maximum 60.
templateRefbodyoptionalWhich authentication template to send. Omit it when the channel has one, which is what 'Set up OTP' creates.
senderbodyoptionalWhich number carries the code: 'own' (your connected WhatsApp number) or 'frontdesk' (ours). Omit it and your workspace's own setting decides, which is 'own' unless you changed it — so an integration written before this existed is unaffected. On 'frontdesk' the code arrives from FrontDesk rather than your business, because an authentication template's wording is fixed by WhatsApp and has no place for your name. Cannot be combined with templateRef: the template on our number is ours.
Returns StoreOtpSendView
curl -X POST https://api.frontdesk.africa/v1/store/otp/send \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "value",
"code": "optional",
"purpose": "optional",
"expiresInMinutes": "optional",
"templateRef": "optional",
"sender": "optional"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/otp/send', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"to": "value",
"code": "optional",
"purpose": "optional",
"expiresInMinutes": "optional",
"templateRef": "optional",
"sender": "optional"
}),
});
const data = await res.json();
{
"challengeRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"to": "+234801****678",
"expiresAt": "2026-08-18T09:20:11.000Z",
"chargeRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"quote": {
"category": "authentication",
"market": "Nigeria",
"baseUsdMicros": 1,
"markupPct": 1,
"chargeMinor": 200000,
"currency": "NGN",
"fxRate": 1
},
"sender": "frontdesk"
}
Check a code your customer typed against a challenge we minted
200 means verified. A failure is a RESULT rather than an API error, so it answers 422 with {verified:false, reason} instead of the usual error envelope — branch on `verified` either way. `reason` is invalid, expired or too_many_attempts. An unknown challengeRef, an expired challenge and a wrong code all answer identically on purpose, so this cannot be used to discover which refs exist. Five wrong guesses burns the challenge; so does one right one, because a code is good exactly once — send a new one rather than retrying. If you supplied your own `code` on send, this answers 409: we never saw it in the clear, and telling you `verified: true` for a check we cannot actually make would be worse than saying so.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Body parameters
challengeRefbodyrequiredThe challengeRef from /v1/store/otp/send.
codebodyrequiredExactly what your customer typed.
Returns StoreOtpVerifyView
curl -X POST https://api.frontdesk.africa/v1/store/otp/verify \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"challengeRef": "value",
"code": "value"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/otp/verify', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"challengeRef": "value",
"code": "value"
}),
});
const data = await res.json();
{
"verified": false,
"reason": "expired"
}
Other
The payment rails this workspace can charge, so you can render your own picker
Headers
AuthorizationstringrequiredSet to Bearer fd_pk_live_xxx.
OriginstringrequiredMust be one of the origins registered on the key. Sent automatically by the browser.
Query parameters
currencyqueryoptionalDefaults to the store primary.
Returns string[]
curl https://api.frontdesk.africa/v1/store/payment-methods \
-H "Authorization: Bearer fd_pk_live_xxx" \
-H "Origin: https://yourbrand.com"
const res = await fetch('https://api.frontdesk.africa/v1/store/payment-methods', {
headers: {
Authorization: 'Bearer fd_pk_live_xxx',
'Origin': 'https://yourbrand.com',
},
});
const data = await res.json();
[
"paystack"
]
Attach your own signed-in user to this workspace's contacts, so their purchases and plans line up under one person
Only link identities your own auth verified (a confirmed email or OTP-verified phone) — this endpoint trusts your assertion because the key already proves you are the merchant's backend. Idempotent per externalRef: repeat calls return the SAME link and alreadyLinked=true, and the response shape is identical whether the contact existed before or not, so nothing here can be used to probe who is already a customer. Fires the customer.linked webhook exactly once, on the link. With a TEST key this validates fully and returns synthetic test_cus_/test_con_ refs that resolve to nothing, creating no contact.
Headers
AuthorizationstringrequiredSet to Bearer fd_sk_live_xxx.
Body parameters
emailbodyrequiredThe email YOUR auth system verified. Never a guessed or unverified address.
externalRefbodyrequiredYour own user id (e.g. a Supabase Auth user id). The idempotency key: the same value always returns the same link.
namebodyoptionalDisplay name. Fills an empty contact name, never overwrites a set one.
phonebodyoptionalOptional phone for the contact.
Returns StoreCustomerLinkView
curl -X POST https://api.frontdesk.africa/v1/store/customers/link \
-H "Authorization: Bearer fd_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "value",
"externalRef": "value",
"name": "optional",
"phone": "optional"
}'
const res = await fetch('https://api.frontdesk.africa/v1/store/customers/link', {
method: 'POST',
headers: {
Authorization: 'Bearer fd_sk_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"email": "value",
"externalRef": "value",
"name": "optional",
"phone": "optional"
}),
});
const data = await res.json();
{
"customerRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"contactRef": "019f6f85-efbd-75e4-83e4-c2778a318189",
"alreadyLinked": false
}