Appearance
public-website
34 endpoints.
GET /api/v1/public/website/resolve
Resolve Instance
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
domain | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/config
Get Config
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/send-otp
Send Otp
Generate a 6-digit OTP, store its bcrypt hash, and email it.
Always returns 200 regardless of whether the email exists — prevents account enumeration. If no customer matches, a bare AccCustomer row is created (email only, status=active) so the OTP flow works for new shoppers too.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
}
},
"type": "object",
"required": [
"email"
],
"title": "SendOtpIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/verify-otp
Verify Otp
Verify a 6-digit OTP and return a JWT + customer profile.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"code": {
"type": "string",
"maxLength": 6,
"minLength": 6,
"title": "Code"
}
},
"type": "object",
"required": [
"email",
"code"
],
"title": "VerifyOtpIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/auth/me
Auth Me
Return the authenticated customer's profile, or 401.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
authorization | header | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{
"properties": {
"email": {
"type": "string",
"title": "Email"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"company_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Company Name"
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Phone"
},
"customer_group": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Customer Group"
},
"is_reseller": {
"type": "boolean",
"title": "Is Reseller",
"default": false
}
},
"type": "object",
"required": [
"email"
],
"title": "WebsiteCustomerInfo"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/portal-link
Portal Link
A fresh one-time link that opens the portal already signed in, for a customer who is ALREADY signed in on the website.
/auth/login hands back portal_url once, and it is good for 60 seconds — so it only helps the moment the password is typed. Owner, 2026-09-21: "your portal button must auto log into the portal": the header's Customer Portal item is clicked hours or days later, on a 30-day website session, and until this it opened the portal's front door, which asked for the password again. This mints the same link on demand from the website session alone.
Authenticated by delegating to :func:auth_me, so the token checks are the one set — valid, bound to this site, not revoked by a sign-out, customer still on the books — and an ERP-backed tenant is proxied to the books the same way, where the signing key lives. portal_url is null (not an error) whenever it cannot be minted, exactly as at login: the caller then just opens the portal's front door.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
authorization | header | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/login
Login
Authenticate with the SAME credential as the customer portal.
Owner, 2026-09-09: "sign in on website and portal must be one, and must not be an sms, but a username and password. username = mobile number" (for VehicleDB; "all sites except vehicle db will work with email as username"). So this is the one door: the portal's own User row is the credential store for both, and a successful sign-in here also hands back portal_url — a 60-second one-time link that opens the portal already signed in, which is what the website's "Login to Portal" button opens in a new tab.
Cross-DB by nature: the credential lives in the tenant's portal identity store (portal_identity_sessionmaker — its own portal site for bs/lubb/ vdb, a remote DSN for tc, whose portal moved to the pbx box), while the customer row lives on the books. Neither is necessarily this site's own database.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Email"
},
"username": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Username"
},
"password": {
"type": "string",
"minLength": 1,
"title": "Password"
}
},
"type": "object",
"required": [
"password"
],
"title": "LoginIn",
"description": "One credential, whatever the tenant calls it.\n\n``username`` is what the website sends now; ``email`` is the field name\nit sent before 2026-09-09 and is still accepted, because the value was\nalways just \"what the customer typed\" and renaming a wire field is not\nworth breaking a release that is mid-deploy. Either may carry an EMAIL\nADDRESS (every tenant but VehicleDB) or a MOBILE NUMBER (VehicleDB, whose\ncustomers have no address — see app.services.user_identity, which owns the\nrule for telling the two apart)."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/forgot-password
Forgot Password
Email a customer a link to set a new website password.
Owner, 2026-09-21: Cloud Tools (no portal) had no way to recover a password, and its resellers sign in on a password that lives on their customer row (login step 1a). This resets THAT credential. It does not touch a portal user's password — those are reset on the portal, which is why the sign-in dialog only offers this where there is no portal.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"origin": {
"type": "string",
"maxLength": 255,
"minLength": 8,
"title": "Origin"
},
"store_name": {
"anyOf": [
{
"type": "string",
"maxLength": 200
},
{
"type": "null"
}
],
"title": "Store Name"
}
},
"type": "object",
"required": [
"email",
"origin"
],
"title": "ForgotPasswordIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/reset-password
Reset Password
Set a new website password from a link :func:forgot_password mailed.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"token": {
"type": "string",
"maxLength": 4000,
"minLength": 20,
"title": "Token"
},
"password": {
"type": "string",
"maxLength": 128,
"minLength": 8,
"title": "Password"
}
},
"type": "object",
"required": [
"token",
"password"
],
"title": "ResetPasswordIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/auth/sso-exchange
Sso Exchange
Exchange a valid portal JWT for a website JWT — enables SSO from the portal to the website without re-entering credentials.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"portal_token": {
"type": "string",
"title": "Portal Token"
}
},
"type": "object",
"required": [
"portal_token"
],
"title": "SsoExchangeIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/reseller/users
List Reseller Users
The signed-in reseller's own end-users, for the checkout's "who is this order for?" picker.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
authorization | header | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/reseller/users
Create Reseller User From Website
Create a new end-user under the SIGNED-IN reseller, from the shop.
Mirrors app.api.reseller_settings.create_reseller_user — portal login (User + Tenant + owner Membership) on the portal site, AccCustomer with customer_group="Reseller User" + reseller_id on the books site (_books_db(site) — site's own database when they're the same, cross-site otherwise), plus the separate customer-portal site fronting that books site (_portal_instance_for), so up to three different databases can be touched here rather than one coming from Depends(get_db).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
authorization | header | no | ||
x-website-secret | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Phone"
},
"password": {
"anyOf": [
{
"type": "string",
"maxLength": 200,
"minLength": 8
},
{
"type": "null"
}
],
"title": "Password"
}
},
"type": "object",
"required": [
"name",
"email"
],
"title": "NewResellerEndUserIn"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/pages/{slug}
Get Page
One enabled content page's full HTML body, for storefront-web's /[slug] route. 404 if the page isn't a known standard page, isn't built, or is disabled.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string | |
instance | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/articles
List Articles
Published blog articles, newest first — lubb-sites' /blog listing and the "Latest articles" rail on a home page. Cards only (excerpt, not body); the full article comes from /articles/{slug}.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
limit | query | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/articles/{slug}
Get Article
One published article's full HTML body, for /blog/{slug}. 404 on an unknown slug or an article that is still a draft.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
slug | path | yes | string | |
instance | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/catalogue
Get Catalogue
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
price_list | query | no | Quote every item off THIS price list by name instead of the public customer waterfall — e.g. 'Reseller' for the reseller programme page, which advertises trade rates on purpose. | |
books_only | query | no | boolean | Price this site's catalogue off the BOOKS alone, with no local website row. For a websites-instance refresher pulling its read model: the website — display_mode, branding, pages — lives on that instance now, and this hub may have no website row at all. |
vat_multiplier | query | no | The CALLER's VAT display multiplier (see _vat_multiplier). Only read with books_only, and for the same reason as hide_prices_for_anonymous: it is derived from the website's branding, which is website data this hub may no longer hold. | |
hide_prices_for_anonymous | query | no | boolean | The CALLER's auth_required_for_pricing. Only read with books_only: the policy belongs to the website, so the website has to state it rather than have this endpoint look it up. |
x-website-secret | header | no | ||
authorization | header | no |
200 — Successful Response
json
{
"type": "array",
"items": {
"properties": {
"item_code": {
"type": "string",
"title": "Item Code"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name"
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Description"
},
"seo_title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Seo Title"
},
"seo_description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Seo Description"
},
"image_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Image Url"
},
"category": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Category"
},
"product_line": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Product Line"
},
"price": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Price"
},
"currency": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Currency"
},
"annual_price": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Annual Price"
},
"price_bands": {
"anyOf": [
{
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Price Bands"
},
"annual_price_bands": {
"anyOf": [
{
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Annual Price Bands"
},
"band_prices": {
"anyOf": [
{
"additionalProperties": {
"type": "number"
},
"type": "object"
},
{
"type": "null"
}
],
"title": "Band Prices"
},
"uom": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Uom"
},
"requires_shipping": {
"type": "boolean",
"title": "Requires Shipping",
"default": false
},
"stock_qty": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Stock Qty"
},
"features": {
"items": {
"type": "string"
},
"type": "array",
"title": "Features",
"default": []
},
"feature_values": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"title": "Feature Values"
},
"badge": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Badge"
},
"setup_fee": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Setup Fee"
},
"video_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Video Url"
},
"video_caption": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Video Caption"
},
"options": {
"items": {
"properties": {
"label": {
"type": "string",
"title": "Label"
},
"choices": {
"items": {
"$ref": "#/components/schemas/CatalogueOptionChoice"
},
"type": "array",
"title": "Choices"
}
},
"type": "object",
"required": [
"label",
"choices"
],
"title": "CatalogueOptionGroup",
"description": "One picker on a product page — every ``AccItemOption`` row sharing a\n``group_label`` on one parent item. Single-select: the website removes a\nsibling choice from the cart when another is picked."
},
"type": "array",
"title": "Options",
"default": []
}
},
"type": "object",
"required": [
"item_code",
"name",
"description",
"image_url",
"category",
"product_line",
"price",
"currency"
],
"title": "CatalogueItem"
},
"title": "Response Get Catalogue Api V1 Public Website Catalogue Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/checkout
Checkout
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
authorization | header | no |
Request body
json
{
"properties": {
"items": {
"items": {
"properties": {
"item_code": {
"type": "string",
"title": "Item Code"
},
"qty": {
"type": "number",
"exclusiveMinimum": 0,
"title": "Qty"
},
"billing_period": {
"type": "string",
"enum": [
"monthly",
"annual"
],
"title": "Billing Period",
"default": "monthly"
},
"line_note": {
"anyOf": [
{
"type": "string",
"maxLength": 140
},
{
"type": "null"
}
],
"title": "Line Note"
}
},
"type": "object",
"required": [
"item_code",
"qty"
],
"title": "CheckoutItemIn"
},
"type": "array",
"maxItems": 100,
"minItems": 1,
"title": "Items"
},
"email": {
"type": "string",
"maxLength": 255,
"title": "Email"
},
"name": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Name"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Phone"
},
"shipping_option": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Shipping Option"
},
"shipping_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Shipping Address"
},
"origin": {
"type": "string",
"maxLength": 500,
"title": "Origin"
},
"recurring": {
"type": "boolean",
"title": "Recurring",
"default": false
},
"reseller_user_id": {
"anyOf": [
{
"type": "string",
"maxLength": 36
},
{
"type": "null"
}
],
"title": "Reseller User Id"
}
},
"type": "object",
"required": [
"items",
"email",
"origin"
],
"title": "CheckoutIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/credits/account
Credits Account
Does this mobile number already have a VehicleDB account?
The checkout asks before taking payment, because the mobile number IS the app's identity (:class:app.models.vehicledb.VdbAppUser) — bought credits land on the wallet behind this number, so the buyer needs to see whether that is an account they already have (top-up) or a new one being opened. Matching on the email address instead would happily credit a wallet nobody can sign in to.
Deliberately thin: whether an account exists, a display name, and a MASKED email. This route is reachable with nothing but the website secret (held by the renderer, not the browser), and a full name+email per number would make it a lookup service for anyone who ever obtains that secret. Rate limited on top of that, since the answer is a yes/no keyed by phone number.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
mobile | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/contact
Contact
Storefront "Get in Touch" enquiry — emails the site's own support inbox (branding.support_email) via the per-site SMTP overlay. Best-effort like every other send here: a disabled/misconfigured SMTP returns a clean 422 rather than 500, so the website can show a real message.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 200,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"subject": {
"anyOf": [
{
"type": "string",
"maxLength": 200
},
{
"type": "null"
}
],
"title": "Subject"
},
"message": {
"type": "string",
"maxLength": 5000,
"minLength": 1,
"title": "Message"
}
},
"type": "object",
"required": [
"name",
"email",
"message"
],
"title": "ContactIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/boxseats-trial
Boxseats Trial
Request the free 10-minute Box Seats instant web trial.
Records the lead and EMAILS a one-time sign-in link; nothing is provisioned here (owner, 2026-09-07: "send trial auto login link to email, to ensure the email is valid"). The line is created when that link is redeemed at /boxseats-trial/activate below.
Answers {ok, reason, message}; message is written to be shown as-is. reason is sent on success and already_claimed for an address that has already ACTIVATED a trial — a row that was mailed and never clicked is re-sent to, not refused.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
authorization | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 200,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Phone"
},
"province": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "Province"
},
"age_group": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "Age Group"
},
"watch_on": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "Watch On"
},
"watches": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
],
"title": "Watches"
},
"latitude": {
"anyOf": [
{
"type": "number",
"maximum": 90,
"minimum": -90
},
{
"type": "null"
}
],
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"type": "number",
"maximum": 180,
"minimum": -180
},
{
"type": "null"
}
],
"title": "Longitude"
},
"accuracy": {
"anyOf": [
{
"type": "number",
"maximum": 20000000,
"minimum": 0
},
{
"type": "null"
}
],
"title": "Accuracy"
}
},
"type": "object",
"required": [
"name",
"email"
],
"title": "BoxseatsTrialIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/boxseats-trial/activate
Boxseats Trial Activate
Redeem an emailed trial link: create the line and hand back the sign-in.
Answers {ok, reason, message} plus username, password, player_url, minutes and expires_at on success. The website redirects the browser to player_url.
Rate-limited on the TOKEN rather than only the IP: a token is a bearer credential arriving complete, so the thing worth throttling is repeated attempts against one, and a shared-NAT IP must not be able to lock out everyone behind it. 256 bits is not guessable — this is belt to that braces, and it also caps a mail client that pre-fetches links in a loop.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
authorization | header | no |
Request body
json
{
"properties": {
"token": {
"type": "string",
"maxLength": 200,
"minLength": 8,
"title": "Token"
}
},
"type": "object",
"required": [
"token"
],
"title": "BoxseatsTrialActivateIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/trial-signup
Trial Signup
Start a self-serve free trial. Answers {ok, reason, message, job_id?}; a refusal (already_claimed) is a 200 with ok: false so the form can show the message as an answer rather than a fault.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 200,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"company": {
"type": "string",
"maxLength": 200,
"minLength": 1,
"title": "Company"
},
"product": {
"anyOf": [
{
"type": "string",
"maxLength": 120
},
{
"type": "null"
}
],
"title": "Product"
},
"product_line": {
"anyOf": [
{
"type": "string",
"maxLength": 120
},
{
"type": "null"
}
],
"title": "Product Line"
},
"notes": {
"anyOf": [
{
"type": "string",
"maxLength": 4000
},
{
"type": "null"
}
],
"title": "Notes"
},
"support_email": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Support Email"
}
},
"type": "object",
"required": [
"name",
"email",
"company"
],
"title": "TrialSignupIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/trial-signup/{job_id}
Trial Signup Status
Where a started trial has got to: {ok, status, message, sign_in_url?}. status ends at ready or failed. Never carries the password.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
job_id | path | yes | string | |
instance | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/quote-request
Quote Request
Submit a basket as a quote request — the quote cart type's checkout.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
authorization | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 200,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Phone"
},
"company": {
"anyOf": [
{
"type": "string",
"maxLength": 200
},
{
"type": "null"
}
],
"title": "Company"
},
"message": {
"anyOf": [
{
"type": "string",
"maxLength": 5000
},
{
"type": "null"
}
],
"title": "Message"
},
"lines": {
"items": {
"properties": {
"item_code": {
"type": "string",
"maxLength": 140,
"minLength": 1,
"title": "Item Code"
},
"qty": {
"type": "number",
"maximum": 100000,
"exclusiveMinimum": 0,
"title": "Qty"
}
},
"type": "object",
"required": [
"item_code",
"qty"
],
"title": "QuoteLineIn"
},
"type": "array",
"maxItems": 100,
"minItems": 1,
"title": "Lines"
}
},
"type": "object",
"required": [
"name",
"email",
"lines"
],
"title": "QuoteRequestIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/domain-search
Domain Search
Live domain-name availability: the four ZARC .za TLDs, plus every international extension the Domain Pricing grid has priced (see _intl_tlds). Read-only (EPP <check>/Cloudflare Registrar Check only) and deliberately thin: check_registration() also returns registrant/nameserver/registrar detail for a TAKEN .za domain, which is real personal information for a domain that may not even be a Lubb customer's — this endpoint keeps only the availability flag from that result before it's ever serialized.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
x-website-secret | header | no |
Request body
json
{
"properties": {
"query": {
"type": "string",
"maxLength": 100,
"minLength": 1,
"title": "Query"
},
"tlds": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array",
"maxItems": 64
},
{
"type": "null"
}
],
"title": "Tlds"
}
},
"type": "object",
"required": [
"query"
],
"title": "DomainSearchIn",
"description": "``tlds`` narrows the search to the extensions the visitor actually has\nswitched on in the website's chip row (lubb-sites\ncomponents/domain-search.tsx). Omitted or empty means every extension we\nsell, which is what this endpoint has always answered — so an older\nwebsite build that doesn't send the field is unaffected.\n\nIt is a FILTER over the offered set, never an extension of it: a name in\nhere that we don't sell is dropped rather than checked, so the field can't\nbe used to spend our ZARC/Cloudflare rate limit on arbitrary lookups, and\nthe \"only ever offer an extension the owner can see we sell\" rule above\nstill holds. Capped because each surviving entry is an EPP round trip."
}200 — Successful Response
json
{
"properties": {
"results": {
"items": {
"properties": {
"domain": {
"type": "string",
"title": "Domain"
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Available"
},
"price": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Price"
},
"currency": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Currency"
}
},
"type": "object",
"required": [
"domain",
"available"
],
"title": "DomainSearchResult"
},
"type": "array",
"title": "Results"
}
},
"type": "object",
"required": [
"results"
],
"title": "DomainSearchOut"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/booking/pbx-offer/slots
Pbx Offer Slots
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
date | query | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/booking/pbx-offer
Create Pbx Offer Booking
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"maxLength": 255,
"minLength": 3,
"title": "Email"
},
"phone": {
"type": "string",
"maxLength": 64,
"minLength": 1,
"title": "Phone"
},
"company": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Company"
},
"start_at": {
"type": "string",
"format": "date-time",
"title": "Start At"
}
},
"type": "object",
"required": [
"name",
"email",
"phone",
"start_at"
],
"title": "PbxOfferBookingIn"
}201 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/orders/{token}
Order Status
The order page's poll — and the shopper's own route into settlement.
Paystack redirects a paying shopper straight here (callback_url is {origin}/order/{pay_token}, set in :func:checkout), so this is the first thing that knows a charge may have landed. It no longer just reports the order's state: while the order is still unpaid it ASKS Paystack whether the charge succeeded and, if it did, settles it into the ERP — Sales Invoice raised and submitted, Activations raised for staff to process — via :func:app.apps.erp.services.accounting_payment_paystack.settle_website_order_by_reference.
Before this, charge.success reaching the webhook (:mod:app.apps.erp.api.paystack) was the single point of failure between a real charge and an invoice: a delivery Paystack never made left the shopper paid, this page spinning "Waiting for payment confirmation…" forever, and nothing raised for anyone to act on. The webhook still runs and is still normally first; both funnel into the same claim-guarded booking, so whichever arrives first wins and the other is a no-op.
origin — the shopper-facing public origin (the renderer passes it, same as checkout does), used for the links in the invoice email settlement sends. Omitted, that falls back to settings.APP_BASE_URL.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
token | path | yes | string | |
instance | query | yes | string | |
origin | query | no | ||
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/brief
Get Brief
The questions, plus whatever this client has already answered.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
token | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/brief
Save Brief
Save the client's answers. Called on autosave and on submit.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"token": {
"type": "string",
"maxLength": 64,
"minLength": 16,
"title": "Token"
},
"answers": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"title": "Answers"
},
"submit": {
"type": "boolean",
"title": "Submit",
"default": false
}
},
"type": "object",
"required": [
"token"
],
"title": "BriefAnswersIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/public/website/reseller-rates
Get Reseller Rates
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
200 — Successful Response
json
{
"type": "object",
"additionalProperties": true,
"title": "Response Get Reseller Rates Api V1 Public Website Reseller Rates Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/book-order
Book Order
Price and book one website order on the books. Returns {order_id, pay_token, grand_total}.
The Idempotency-Key is not optional and not decoration. Putting a network hop in front of order creation adds a failure mode a hub-served website never had: a request that times out after the order was written. Without the key the instance's retry books a second order and the buyer is invoiced twice for one basket.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
authorization | header | no | ||
Idempotency-Key | header | no |
Request body
json
{
"properties": {
"checkout": {
"properties": {
"items": {
"items": {
"properties": {
"item_code": {
"type": "string",
"title": "Item Code"
},
"qty": {
"type": "number",
"exclusiveMinimum": 0,
"title": "Qty"
},
"billing_period": {
"type": "string",
"enum": [
"monthly",
"annual"
],
"title": "Billing Period",
"default": "monthly"
},
"line_note": {
"anyOf": [
{
"type": "string",
"maxLength": 140
},
{
"type": "null"
}
],
"title": "Line Note"
}
},
"type": "object",
"required": [
"item_code",
"qty"
],
"title": "CheckoutItemIn"
},
"type": "array",
"maxItems": 100,
"minItems": 1,
"title": "Items"
},
"email": {
"type": "string",
"maxLength": 255,
"title": "Email"
},
"name": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Name"
},
"phone": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Phone"
},
"shipping_option": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Shipping Option"
},
"shipping_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Shipping Address"
},
"origin": {
"type": "string",
"maxLength": 500,
"title": "Origin"
},
"recurring": {
"type": "boolean",
"title": "Recurring",
"default": false
},
"reseller_user_id": {
"anyOf": [
{
"type": "string",
"maxLength": 36
},
{
"type": "null"
}
],
"title": "Reseller User Id"
}
},
"type": "object",
"required": [
"items",
"email",
"origin"
],
"title": "CheckoutIn"
},
"email": {
"type": "string",
"title": "Email"
},
"currency": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Currency"
},
"lines": {
"items": {
"properties": {
"item_code": {
"type": "string",
"title": "Item Code"
},
"billing_period": {
"type": "string",
"title": "Billing Period",
"default": ""
},
"line_note": {
"type": "string",
"title": "Line Note",
"default": ""
},
"qty": {
"type": "number",
"title": "Qty"
}
},
"type": "object",
"required": [
"item_code",
"qty"
],
"title": "BookOrderLineIn",
"description": "One (item_code, billing_period, line_note) -> qty entry.\n\nA list rather than a dict because the real key is a TRIPLE — a cart can hold\none item at both Monthly and Annual, and two domain registrations are one\nitem_code differing only by the name bought. Collapsing to item_code loses a\nline and charges for one of two; see _price_and_book_website_order."
},
"type": "array",
"maxItems": 100,
"minItems": 1,
"title": "Lines"
},
"website": {
"anyOf": [
{
"properties": {
"shipping_flat_base": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Shipping Flat Base"
},
"shipping_flat_qty": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Shipping Flat Qty"
},
"shipping_flat_extra": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Shipping Flat Extra"
},
"branding": {
"additionalProperties": true,
"type": "object",
"title": "Branding"
}
},
"type": "object",
"title": "BookOrderWebsiteIn",
"description": "The website settings this order has to be priced with, as they stand\non the INSTANCE that is rendering the shop.\n\nFlat-rate courier (``website_settings.courier_shipping``) and the\nVAT-inclusive display flags (``_vat_multiplier``) — everything else the\nbooks half needs it already has. Not optional in practice: defaulting the\nshipping trio to unset would ship every physical order free, and bs alone\nquotes R199 for the first three items."
},
{
"type": "null"
}
]
}
},
"type": "object",
"required": [
"checkout",
"email",
"lines"
],
"title": "BookOrderIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/order-payment-ref
Order Payment Ref
Attach the Paystack reference (or the init error) to a booked order.
Small, but it is what lets the payment webhook find the order it paid for. The instance calls this synchronously and falls back to its outbox if the call fails — see checkout — because an order that never receives its reference is a payment that cannot be matched to anything.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no |
Request body
json
{
"properties": {
"order_id": {
"type": "integer",
"title": "Order Id"
},
"reference": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Reference"
},
"error": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Error"
}
},
"type": "object",
"required": [
"order_id"
],
"title": "OrderPaymentRefIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/public/website/ingest
Ingest
Accept one queued write from a websites instance, exactly once.
Returns 409 with the ORIGINAL result for a key already seen — which the sender treats as success, because that is what it is: the first attempt landed and only the acknowledgement was lost.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
instance | query | yes | string | |
x-website-secret | header | no | ||
Idempotency-Key | header | no |
Request body
json
{
"properties": {
"kind": {
"type": "string",
"maxLength": 32,
"minLength": 1,
"title": "Kind"
},
"payload": {
"additionalProperties": true,
"type": "object",
"title": "Payload"
}
},
"type": "object",
"required": [
"kind",
"payload"
],
"title": "IngestIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}