Public merchant lookup and onboarding
Public, unauthenticated endpoints for storefront merchant lookup and merchant onboarding.
This page covers two unrelated groups of endpoints: the storefront's merchant and catalog lookup, and merchant onboarding.
Storefront merchant lookup
Used by the public storefront (webshop) to render a merchant's branded
page and catalog. Both are CORS-public, unauthenticated, and only return
data for merchants with status: ACTIVE.
Get a merchant by slug
GET /api/merchants/{slug}Response
{
"ok": true,
"data": {
"id": "...",
"slug": "...",
"name": "...",
"logoUrl": null,
"description": null,
"primaryColor": null,
"secondaryColor": null,
"currency": "EUR",
"homeUrl": null,
"termsUrl": null,
"siftSiteId": null,
"analyticsId": null,
"popularGiftCardAmount": null
}
}termsUrl is fetched from the card network's program details at request
time; if that lookup fails, it's null rather than failing the whole
request. Cache-Control: no-store is set.
Errors
| Status | Cause |
|---|---|
| 404 | No merchant matches slug, or the matched merchant's status is not ACTIVE. |
| 500 | Unexpected internal error. |
Get a merchant's catalog by slug
GET /api/merchants/{slug}/catalogReturns the merchant's sellable gift card offers. Refund cards are excluded (they are internal-use-only and can't be auto-issued through this storefront flow).
Response
{
"ok": true,
"data": [
{
"id": "...",
"merchantId": "...",
"name": "...",
"description": null,
"imageUrl": null,
"type": "GIFTCARD_DIGITAL",
"currency": "EUR",
"priceOptions": [2500, 5000, 10000],
"minValue": 2500,
"maxValue": 10000
}
],
"meta": { "popularAmount": null }
}priceOptions is in minor units. For fixed-price offers it's a single
value; for variable-pricing offers with no explicit denominations set, it's
auto-generated from the min/max range using "nice" step sizes (roughly 4-6
options), so it may not match what the merchant actually configured if
they intended a continuous range rather than fixed steps.
Errors
Same as Get a merchant by slug.
Onboarding
Most merchants never call these directly
Onboarding is normally completed in the browser through the 4-step sign-up flow (Company information, Directors, Documents, Review & submit) described in Getting started: creating an account. The endpoints below are what that flow uses.
All onboarding submission and document endpoints below require an authenticated session (created during account signup, before KYB submission); only the status-check lookup is fully public.
Save or submit onboarding details
POST /api/onboarding/submitSaves a draft of the company/KYB onboarding form, or performs a full
submission. Only allowed while the merchant's status is DRAFT or
WAITING_FOR_INFO.
Request body
| Field | Type | Required for draft | Required for submit |
|---|---|---|---|
isDraft | boolean | no (defaults to a full submit if omitted/false) | n/a |
registrationNumber | string | no | yes |
vatNumber | string | no | yes |
tradingName | string | no | no |
currency | string | no | no (defaults to EUR) |
numberOfLocations | string | no | no (defaults to "0") |
legalAddress.line1 / .city / .postalCode / .country | string | no | yes |
legalAddress.state | string | no | no |
directors | array of { name, role } | no | yes, at least one |
documents | array of { type, url, name? } | no | no |
termsAccepted | boolean | no | yes |
privacyAccepted | boolean | no | yes |
On a full submission (isDraft not true), this also creates or refreshes
an IDenfy verification session and emails the merchant a status update with
the verification link; both are best-effort and a failure here does not
fail the submission itself.
Response
{ "ok": true }Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 400 | Session user has no associated merchant; body fails to parse; or (full submit only) required fields are missing, or terms/privacy were not accepted. |
| 403 | Merchant status is not DRAFT or WAITING_FOR_INFO (application is under review and cannot be edited). |
| 500 | Unexpected internal error. |
Upload an onboarding document
POST /api/onboarding/uploadUploads a KYB document (for example, Certificate of Incorporation or director ID) to object storage under a tenant-scoped key.
Request
multipart/form-data with a single field:
| Field | Type | Required |
|---|---|---|
file | file | yes |
Response
{
"ok": true,
"url": "https://<bucket>.s3.<region>.amazonaws.com/<merchantId>/onboarding/documents/<uuid>-<name>.<ext>",
"previewUrl": "https://...(signed, expires in 1 hour)",
"key": "<merchantId>/onboarding/documents/<uuid>-<name>.<ext>",
"originalName": "...",
"filename": "<uuid>-<name>.<ext>"
}url is the permanent (unsigned) S3 URL, intended for storing in the
documents array passed to onboarding/submit. The bucket is private, so
this URL alone is not browsable; use previewUrl for immediate display, or
Get a signed document URL later.
Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 400 | Session user has no associated merchant, or no file provided. |
| 500 | Upload failed. |
Get a signed document URL
GET /api/onboarding/document?url=<S3 object URL>Exchanges a previously stored document URL for a short-lived signed URL (60 seconds), since the underlying bucket is private.
Query parameters
| Param | Required | Notes |
|---|---|---|
url | yes | Must be a virtual-hosted-style S3 URL for the configured bucket. |
Response
{ "ok": true, "url": "https://...(signed, expires in 60 seconds)" }Errors
| Status | Cause |
|---|---|
| 401 | No session. |
| 400 | Missing url, or url doesn't parse as a valid S3 URL for the configured bucket. |
| 403 | Non-admin session user has no associated merchant, or the document's storage key does not belong to their merchant (tenant check on the key prefix). Admins bypass this check. |
| 500 | Failed to generate the signed URL. |
Check onboarding status by email
POST /api/public/merchant/onboarding-checkFully public, unauthenticated. Rate-limited per client IP and per email address (separately). Used for a "what's the status of my application" self-service lookup, without requiring a login.
Request body
| Field | Type | Required |
|---|---|---|
email | string (valid email) | yes |
Response
{
"ok": true,
"data": {
"recognized": true,
"activated": false,
"merchantStatus": "KYB_IN_PROGRESS",
"summary": {
"text": "...",
"nextStep": "..."
}
}
}recognized is false if no user matches the email at all (the data
shape is narrower in that case: just recognized and summary, no
activated/merchantStatus). merchantStatus is one of DRAFT,
SUBMITTED, WAITING_FOR_INFO, KYB_IN_PROGRESS, DECISION_PENDING,
ACTIVE, INACTIVE, SUSPENDED, DECLINED, or null if the email
matches an admin account with no merchant, or a user with no merchant
profile linked yet. summary.text is a plain-language status message
intended to be shown directly to the merchant; it never echoes raw status
codes. The response intentionally never reveals whether an email exists in
a way that would help enumerate accounts beyond the generic "not found"
message.
Errors
| Status | Cause |
|---|---|
| 429 | Rate limit exceeded for this IP or this email address; Retry-After header indicates the wait. |
| 400 | email missing or not a valid email address. |
| 500 | Unexpected internal error. |