Everything Teculiar does, reachable from a website we do not host: browse the catalog, search domains, check out, pay invoices, add funds, manage services, open and answer tickets, read content, manage an account.
Base URL: https://api.<your-domain>/api/v1/public/… (or https://<tenant>.teculiar.net/api/v1/public/…). Generated reference: openapi/public-v1.json.
| You want | Read |
|---|
| A JS SDK or drop-in widgets | sdk-widgets.md |
| Event notifications to your server | webhooks.md |
| Every route, internal ones included | api-endpoints.md |
| How the contract is enforced in CI | api-contract.md |
It is a projection, not a copy
The public API is not a hand-maintained parallel implementation. Every route in the product carries an explicit @PublicRoute or @InternalRoute decision, the API refuses to boot if one does not, and /api/v1/public/* is a second door onto the very same handlers. The OpenAPI spec, the SDK and this documentation are generated from those decisions.
That matters to you as a consumer: there is no second implementation to fall behind, so a fix to the product is a fix to the public API on the same day.
Authenticating
Two things authenticate a call, and they answer different questions.
| What it proves | Where it lives |
|---|
API key (Teculiar-Key) | which site is calling | pk_ in page source, sk_ on your server |
Customer token (Authorization: Bearer) | which person the data belongs to | obtained from /public/auth/login |
A key authorizes the site; a token authorizes the person. A publishable key may hold invoices:read, but every route carrying that scope also demands a customer token — so the key alone never reads anyone's data.
pk_ publishable
An identifier, not a secret. It sits in page source by design, so the control is its **origin allowlist**: a request from an origin you have not registered is a 403. Register origins at Admin → Settings → API keys.
sk_ secret
Server-side only. A request carrying an Origin header is refused outright — an Origin means a browser sent it, and a secret key in a browser has already leaked. Rotate it if you see that error.
Unknown, revoked and expired keys all fail identically, so probing tells you nothing.
Scopes
Three groups. The axis is not "is the data public" but "is access gated by something other than the key".
Anonymous — the key is the only gate, so the data behind it is public: catalog:read, content:read, domains:search, inquiries:write, auth:write, checkout:write, payment-methods:read.
Customer — a pk_ may hold these, but every route carrying one also requires a customer token: customers:read|write, invoices:read|pay, orders:read, payments:write, services:read|manage, domains:manage, tickets:read|write.
Secret — never holdable by a pk_: webhooks:manage.
**Credentials never answer a pk_.** A route that hands out a credential — a domain's transfer code (GET /services/:id/transfer-code, and POST /services/:id/domain-lock when it unlocks), or a secret-class module action — needs the customer token sent with an sk_ key from your server. With a pk_ it is a 403, whatever scopes the key holds: a script injected into a page that carries a pk_ would hold the customer's token too.
Idempotency
Send an Idempotency-Key header (1 to 200 characters of A-Z a-z 0-9 - _, a new value for each intended request) on POST /orders/checkout, POST /orders/{id}/pay, POST /billing/invoices/{id}/pay, POST /billing/add-funds, POST /storefront/inquiries and POST /tickets. The operations that take it are marked x-idempotent in the spec.
- The same key and the same body again returns the first answer, with the same status, and the header
Idempotent-Replayed: true. Nothing runs twice: no second order, account, invoice or charge. - The same key with a different body (or from a different customer) is
422 IDEMPOTENCY_KEY_REUSED. - The same key while the first request is still being handled is
409 IDEMPOTENCY_IN_PROGRESS; retry after a moment. A request that never finished (the server restarted) is forgotten after five minutes. - An answer of 2xx or 4xx is kept for 24 hours. A 5xx, or a crash, leaves nothing behind, so the retry runs. Only a hash of your request body is stored, never the body; the stored answer is the one you were sent.
- Without the header nothing changes. A malformed key is
400 IDEMPOTENCY_KEY_INVALID.
The SDK sends a fresh key on each call of those operations and repeats the call with the same key after a timeout or a 5xx. It never repeats any other write.
Rate limits
Per key and per IP, enforced globally. The tenant-facing numbers are settings under Admin → Settings → Integrations, each capped by a platform ceiling that protects the platform rather than the tenant — domain search is the strict one, because every search costs outbound lookups at the registries from our IP address.
The ceiling itself is operator-side, stored in the control-plane and unreachable from any tenant-facing route. Your admin shows you the ceiling beside each field so a clamped value is explained rather than mysterious; raising one is a conversation with us, not a setting.
A 429 carries Retry-After. The SDK surfaces it as a typed error rather than retrying blindly.
Compatibility
The published surface is additive-only. Fields are added, never removed or renamed without a version bump — and that is enforced, not promised: scripts/api-contract-check.ts diffs the generated spec against the last released snapshot and fails the build when a published field disappears.
A Teculiar-Version date header is reserved for a future breaking change. Until one exists, there is nothing to pin.
Request inputs. docs/openapi/public-v1.json carries each write operation's requestBody and each operation's query parameters, written in public-inputs.ts and checked against what the handlers actually read. The gate also refuses, against the last release, a removed input, an optional input made required, a new required input, and a removed allowed value (an enum). Fields marked required in the spec are the ones the API refuses a request without.
Response types. A field in the spec carries a JSON type where the shape says one (string, integer, number, boolean, object, array, and null for fields that can be empty); the SDK's types follow, so a call comes back typed. The catalogue, the bootstrap, the order preview and checkout, the status route, posts, the module library, the name check and the plans are typed in full. Every published field is still optional in the SDK's type, and a type is part of the contract like a name.
Caching. Anonymous reads carry an ETag and Cache-Control: public, max-age=60. Anything requiring a customer token is private, no-store — a per-customer response must never sit in a shared cache.
Bootstrap: GET /storefront/i18n
One small cached call tells a site everything it needs for languages and money. Scope catalog:read, no sign-in; the public cache keeps it for 60 seconds.
{
"main": "en",
"languages": ["en", "de"],
"installed": ["de", "en", "tr"],
"fallback": "en",
"meta": {
"en": { "bcp47": "en-US", "nativeName": "English", "flag": "us", "defaultCurrency": "USD" },
"de": { "bcp47": "de-DE", "nativeName": "Deutsch", "flag": "de", "defaultCurrency": "EUR" }
},
"currencies": {
"main": "USD",
"list": ["USD", "EUR"],
"rates": { "EUR": { "rate": 0.92, "buffer": 0, "bufferEnabled": false } }
},
"serverTime": 1760520000000,
"cookieDomain": null
}
languages is what the store serves, main first. meta has one entry per served language (not per installed pack). defaultCurrency is the currency the language implies, taken the way the Teculiar themes take it: the language pack's own, else the main language's. A site uses it only when the store lists that currency.currencies.list is main first. A currency without a valid exchange rate (a number above 0) is never listed. rates holds one entry for each listed non-main currency. Rates and buffers are display data, the same as in /storefront/settings; the server still prices every order.serverTime is the server's clock in milliseconds when the answer was built. The answer is cached, so it can be a few minutes old; the SDK corrects for that.cookieDomain is null until a tenant registers its own website's host (parity L4).
These answer on teculiar.com (and on the platform's own lab); on any other store they are 404.
GET /platform/names/check?name= (scope catalog:read, 30 a minute) says whether a system name (<name>.teculiar.net) can be claimed: { name, available, reason, suggestions, rules }. reason is null, invalid, reserved, not_for_sale, taken (a system has it) or held (another unpaid order is paying for it). suggestions (at most 3) appear for a valid name that is not available. rules is the grammar: 1 to 31 characters, letters, digits and hyphens, not starting or ending with a hyphen.POST /orders/preview and POST /orders/checkout refuse a name that is not available with 409 and { code: "NAME_UNAVAILABLE", name, reason }. Checkout holds the name until the invoice is due, so the name a buyer pays for is the name they get. The hold ends when the invoice is deleted or the system exists.GET /storefront/modules?locale= and GET /storefront/modules/{name}?locale= list the module library: name, label and description in that language (English when there is no translation), kind, families, capabilities, pricing (free or paid) and hasPanel. The single route adds doc: the module's guide, { language, markdown }, or null. Platform-only modules are never listed.GET /platform/plans?locale= (scope catalog:read) lists the four Teculiar plans from the shared registry: key, name, summary (asked language, English fallback), currency, monthlyCents, yearlyCents, limits, features, ceilingProfile. The pricing page reads it; no limit is typed by hand.
Add-ons in the catalogue
GET /storefront/products gives each product an addOns list (active ones only): id, slug, name, nameTranslations, description, descriptionTranslations, amountCents, setupFeeCents, billingCycle, recurring and maxQuantity (99). Send the id and a quantity in an order line's addOns: [{ addOnId, quantity }] at preview and checkout. What buying one changes on the service is internal and is not published.
Order fields
A product whose module asks the buyer for something (a Teculiar plan: system name, brand, administrator name and email) lists it in configurationFields on GET /storefront/products: key, type (text, email or subdomain), required, maxLength, pattern, label and help for each served language, and checkUrl (the route that says whether a subdomain is free, otherwise null). Send the values in the order line's configuration under those keys. Preview and checkout refuse a line that misses a required field or breaks pattern or maxLength with 400 and { code: "ORDER_FIELD_INVALID", field: "<key>", reason } (required, invalid or too_long).
Checkout status
POST /orders/checkout answers with the checkout as the invoice behind it and a status token:
{ "invoice": { "id": "…", "invoiceNumber": "N-100821", "status": "PENDING", "currency": "EUR", "totalCents": 741, "dueAt": "…" },
"order": { "id": "…" },
"statusToken": "…" }
order.id is the checkout id (the invoice id) until payment creates the order. The statusToken is shown once; only its hash is kept. Pay with POST /orders/{id}/pay (the answer carries invoice.paymentRedirectUrl when the gateway needs the buyer, and invoice.attemptStatus when an attempt failed, was cancelled or expired).
A guest's page follows the checkout without signing in: GET /orders/{id}/status?token=<statusToken> (scope checkout:write, 60 requests a minute).
{ "status": "provisioning",
"items": [ { "type": "MANAGED_SERVICE", "productName": "Branded", "status": "pending", "externalId": null, "links": [] } ] }
status is awaiting_payment, provisioning (paid, something is not active yet), ready (every item active), failed or cancelled. An item is pending, active or failed; once active it carries the service's externalId and a portal link to its page. No names, emails, addresses or amounts are in this answer. A wrong token and an unknown checkout are the same 404.
Return URLs
returnUrl and cancelUrl on payment calls are validated against the hosts you have verified plus the origins registered on the calling key. Anything else is a hard 400, never a silent fallback.
This is strict on purpose. An open redirect on a payment flow is a phishing gift: it lets an attacker send your customer to a lookalike checkout immediately after a real payment, on a link that genuinely came from you. Host equality on a parsed URL — shop.example.com.evil.test and shop.example.com@evil.test are both refused.
Where the buyer goes, and when. returnUrl and cancelUrl are accepted by POST /orders/{id}/pay and POST /billing/invoices/{id}/pay. Neither is given to the payment gateway: the gateway always returns to the store's own payment-return page, which captures the payment (PayPal, Mollie) and creates the order, and only then sends the buyer on. Your address is stored with the checkout and used after that. It gets two query parameters added, order (the checkout id) and result, one of paid, pending (for example SEPA, still being processed), failed or cancelled. A cancelled payment goes to cancelUrl when you gave one, otherwise to returnUrl. The address never carries a login code or token: the buyer's sign-in stays on the store's page. Without returnUrl the buyer stays on the store's own pages, as before. Both returnUrl and cancelUrl are checked the same way.
The framed checkout has no key. <teculiar-checkout> pays from inside the store's own frame, so no key's origins apply: its return-url and cancel-url must be on a host you verified (a *Your own website* host in Admin → Domains, or one of the store's own hosts). A site that is not verified gets the same 400, shown in the frame. Calls made with a key (the headless flow) may also use the key's origins.
Results by provider (checked live against Mollie's test mode and PayPal's sandbox): a paid payment arrives as result=paid; a payment the provider refused, or that expired, as failed; a buyer who presses back at Mollie, or "Cancel and return" at PayPal, as cancelled. Nothing is paid in the last two and the invoice stays payable.
POST /storefront/inquiries — no customer token required, no API key required (anonymous). Creates a ticket in the tenant's support queue and returns a ticket record.
POST /api/v1/public/storefront/inquiries
Content-Type: application/json
{
"name": "Jane Smith",
"email": "jane@example.com",
"subject": "Pre-sales question",
"message": "Does your Starter plan include email hosting?",
"source": "contact"
}
Response 201:
{
"id": "cmt8duoue04z5qr012wlvgu7v",
"publicId": "MED8RD8L",
"status": "OPEN",
"priority": "NORMAL",
"subject": "Pre-sales question",
"createdAt": "2026-08-25T08:08:14.150Z",
"updatedAt": "2026-08-25T08:08:14.150Z",
"replies": [
{
"id": "cmt8duoue04z7qr01jfv92stu",
"body": "Name: Jane Smith\nE-Mail: jane@example.com\n\nDoes your Starter plan include email hosting?",
"internal": false,
"createdAt": "2026-08-25T08:08:14.150Z"
}
]
}
Fields: name (1–100 chars), email, phone (optional, max 30), subject (1–200), message (10–5000). source is "contact" or "inquiry" — routes the ticket to the department the tenant has configured for each. _honey is a honeypot field; a non-empty value signals a bot and the submission is silently discarded.
Customer tickets
These routes require a customer bearer token (Authorization: Bearer <token>) and the tickets:read or tickets:write scope on the API key.
| Method | Path | Scope | What it does |
|---|
GET | /tickets | tickets:read | List the authenticated customer's tickets. Optional ?status= and ?department=. |
POST | /tickets | tickets:write | Open a ticket. Required: subject, body, departmentId. Optional: priority, serviceId. |
GET | /tickets/departments | tickets:read | List departments a ticket can be opened against (id/slug/name/colour). |
GET | /tickets/{id} | tickets:read | Read one ticket with replies. Accepts database id or 8-char public id. Internal notes and staff-only fields are stripped. |
POST | /tickets/{id}/replies | tickets:write | Reply to a ticket. internal field ignored for customer tokens. |
POST | /tickets/{id}/attachments | tickets:write | Attach files (multipart, field files). PNG/JPG/WebP/GIF/AVIF/TIFF and PDF (when the tenant allows it), max 5 files, 25–100 MB each by plan. Every file is re-encoded before storing. Each attachment in a response carries url, a signed link on the files host valid for 1 hour (expiresAt), or null with removedReason. storageKey is deprecated: it now holds the same absolute link. Throttled at 20/min. |
POST | /tickets/{id}/close | tickets:write | Close a ticket. |
priority values: LOW, NORMAL (default), HIGH, URGENT.
GET /tickets/departments is published because departmentId is a required field on POST /tickets — without it the write would be uncallable from the public API. The department's routing mailbox is not included in the response.
Internal notes (internal: true replies) are stripped server-side for customer tokens. A customer cannot post a staff note through a customer token even by setting internal: true in the body.
Staff-only routes (assign, status change, delete, canned replies, admin ticket queue) are @InternalRoute and are not published here.
What is deliberately not published
- The admin surface. This phase makes the customer-facing product headless. Running the admin dashboard headlessly is a different product with a different threat model.
- Module service panels (
services/:id/panel) — the hosting control panel, a domain's transfer lock. They manage mailboxes and reset passwords. They render inside the embedded portal on our origin, or not at all. What a module chooses to publish of its panel is under Module actions. - **
POST /services** — provisions a service with no order and no invoice behind it. The public path to a new service is orders/checkout, which prices it and raises the invoice. - Webhook endpoint CRUD. A key that could register a receiving URL is a data-exfiltration channel. Configure endpoints in your admin.
- The SSO exchange/redeem pair. A host-pinned internal primitive, not a grant type.
Module actions
A service's module (its hosting panel, server or registrar) can publish some of its panel actions here. Each action has a risk class, and the class decides how far it may go:
| Class | Route | Scope | Notes |
|---|
| read | GET /services/:id/actions/:action | services:read | |
| reversible | POST /services/:id/actions/:action (a registrar's: POST /services/:id/domain-actions/:action) | services:manage (domains:manage) | 30 a minute |
| secret | same as reversible | same | never with a pk_ — see Scopes |
| destructive | — | — | never published |
GET /services/:id/actions lists what the service's module publishes: id, label, module, risk, method, path, scope and the body fields it takes. An action the module does not publish on the route you called is a 404, like one that does not exist. The body is flat text fields; the action is named by the path.
| Module | Published |
|---|
| Hetzner Cloud | power-on, reboot, shutdown, power-off (reversible) |
| cPanel | sso (field app: files, email, apps), webmail — single-use sign-on links (secret) |
| Resell.biz, Dynadot | lock-state (read), lock (reversible), unlock (secret: it returns the transfer code) |
| Virtualmin | nothing |
The answer is the provider's receipt — accepted, message, and url for a sign-on link — or, for a registrar, the lock (domain, locked, lockCheckedAt, transferCode after an unlock). Every run is in the tenant's logs; a secret action's answer never is.
Order line configuration
POST /orders/checkout and /orders/preview accept a free-form configuration object on each line, but only these keys are kept — anything else is dropped without an error:
| For | Keys |
|---|
| A domain line | billingCycle, domainAction, extraAttributes, nameServers, transferAuthCode |
| A hosting line and its domain | bundledDomain, domainName, domainUse |
| A Teculiar plan | adminEmail, adminName, brand, subdomain |
Everything that decides what gets provisioned or billed — the hosting plan, the renewal price, a free domain, the provisioning module — comes from the product and the server, never from the request. The list lives in apps/api/src/common/order-line-configuration.ts.
Service lifecycle
A service object's status field is the customer's view of its state. The values form a linear lifecycle; branches only happen at ACTIVE:
| Status | Meaning |
|---|
PENDING | Created and waiting for payment to clear, or waiting for the hosting module to confirm provisioning. |
PROVISIONING | Payment cleared; the hosting module is actively setting up the account. (Virtualmin internally returns "QUEUED" while its job queue processes, which the platform maps to PROVISIONING before writing.) |
ACTIVE | Running normally. |
SUSPENDED | Suspended for non-payment. Restored automatically when the overdue invoice is settled. |
CANCELLED | Scheduled for termination at cancelAt, or already cancelled. Renewal invoices stop. |
TERMINATED | Permanently deprovisioned. The account has been removed from the provider. |
FAILED | Provisioning ran and did not succeed; retryable by admin. |
Suspension grace. An invoice that goes overdue does not suspend the service immediately. The dunning sweep (run once per day by cron) suspends only when a full 24 hours have elapsed since the invoice dueAt. A hosting service (Virtualmin) is additionally disabled at the panel on suspension — the account is not deleted and is re-enabled on payment.
Cancellation is not immediate. POST /services/:id/cancel schedules the end of service at the end of the paid term (renewsAt, falling back to nextDueAt). The cancelAt field in the response carries that date; the service stays ACTIVE until the cron sweep reaches it. POST /services/:id/resume undoes the cancellation if called before that date.
Field strip on the public surface. The service object returned by GET /services and GET /services/:id deliberately omits:
configuration / customFields — opaque provisioning blobs, may carry hosting credentials.moduleName / moduleReference / externalId — internal infrastructure identifiers.suspendedReason — an internal admin note.userId, orderId, orderItemId, initialInvoiceId — foreign keys.
The domainRecords[] sub-array omits eppCode, authCodeHash, registrarProvider, registrarModule, externalId, and dnsRecords for the same reason.
CORS
/api/v1/public/* answers Access-Control-Allow-Origin: * with credentials: false, and accepts Content-Type, Authorization and Teculiar-Key.
credentials: false alongside Authorization is correct rather than contradictory: a bearer token in an explicit header is not a CORS credential, because the browser never attaches it on its own. Only a cookie would be, and this surface reads none.
CORS is not the access control — a non-browser caller ignores it entirely. The control is the key guard.
Errors
Standard HTTP status codes with a JSON body.
| Status | Means |
|---|
400 | The request is malformed, or a return URL failed validation. |
401 | Missing/invalid key, or a customer token is required and absent. |
403 | The key lacks the scope, or the origin is not allowed. |
404 | Not published, or genuinely missing — deliberately indistinguishable. |
409 | A payment for this invoice is already under way, or the invoice has since been paid. |
429 | Rate limited. See Retry-After. |
409 comes from POST /billing/invoices/{id}/pay and is the one status worth handling specially: the request was fine and there is nothing to correct, so a client must not retry it with another payment method. One invoice is collected once, and a payment already in flight at a gateway — a SEPA direct debit takes days to clear — refuses a second one whatever method it names. Show the message to the buyer and stop; the invoice moves to paid on its own when the money arrives. See docs/ordering-and-invoices.md.
Login returns an identical 401 whether the address exists or the password is wrong, and password reset always returns {ok: true}. Registration still reveals that an address is taken — a deliberate UX trade, pre-existing on the hosted storefront, mitigated by a hard throttle rather than claimed to be safe.
Operations
auth
POST /auth/2fa/disable
Turn two-factor off
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
password | body | string | Yes | |
GET /auth/2fa/setup
Begin two-factor setup
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
GET /auth/2fa/status
Whether two-factor is enabled
| Scope | Sign-in | Since |
|---|
customers:read | Customer sign-in | 2026-09 |
POST /auth/2fa/verify
Confirm two-factor setup
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
code | body | string | Yes | |
POST /auth/login
Sign a customer in and return bearer tokens
| Scope | Sign-in | Since |
|---|
auth:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
email | body | string email | Yes | |
password | body | string | Yes | |
scope | body | admin / client | | |
totpCode | body | string | | The six digits from the authenticator, when two-factor is on. |
POST /auth/logout
Revoke a refresh token
| Scope | Sign-in | Since |
|---|
auth:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
refreshToken | body | string | Yes | |
POST /auth/password-reset/confirm
Complete a password reset
| Scope | Sign-in | Since |
|---|
auth:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
password | body | string | Yes | |
token | body | string | Yes | |
POST /auth/password-reset/request
Request a password-reset email
| Scope | Sign-in | Since |
|---|
auth:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
email | body | string email | Yes | |
scope | body | admin / client | | |
POST /auth/refresh
Exchange a refresh token for a new access token
| Scope | Sign-in | Since |
|---|
auth:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
refreshToken | body | string | Yes | |
POST /auth/register
Register a customer account
| Scope | Sign-in | Since |
|---|
auth:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
address | body | object | | |
companyName | body | string | | |
countryCode | body | string | | |
customerType | body | INDIVIDUAL / BUSINESS | | |
email | body | string email | Yes | |
name | body | string | Yes | |
password | body | string | Yes | |
phone | body | string | | |
vatId | body | string | | |
billing
POST /billing/add-funds
Top up the customer's account balance
| Scope | Sign-in | Since |
|---|
payments:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
amountCents | body | integer | Yes | |
iban | body | string | | |
method | body | CREDIT_CARD / PAYPAL / SEPA / CRYPTO / BANK_TRANSFER / IDEAL | Yes | |
GET /billing/invoices
List the customer's invoices
| Scope | Sign-in | Since |
|---|
invoices:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
status | query | string | | Only invoices in this status. |
GET /billing/invoices/{id}
Read one invoice
| Scope | Sign-in | Since |
|---|
invoices:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
GET /billing/invoices/{id}/html
Render an invoice as HTML
| Scope | Sign-in | Since |
|---|
invoices:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
POST /billing/invoices/{id}/pay
Pay an invoice
| Scope | Sign-in | Since |
|---|
invoices:pay | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
cancelUrl | body | string | | Where the buyer ends up after a cancelled payment. Same rule as returnUrl. |
iban | body | string | | |
method | body | CREDIT_CARD / PAYPAL / SEPA / CRYPTO / BANK_TRANSFER / IDEAL | Yes | |
paymentMethodId | body | string | | |
returnUrl | body | string | | Where the buyer ends up after the payment is confirmed, with order and result added. Must be a verified host of the store or an origin of the key. |
useAccountBalance | body | boolean | | |
GET /billing/invoices/{id}/payment-methods
List the payment methods that can pay one invoice
| Scope | Sign-in | Since |
|---|
invoices:pay | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /billing/invoices/{id}/pdf
Download an invoice PDF
| Scope | Sign-in | Since |
|---|
invoices:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /billing/payment-methods
List the customer's saved payment methods
| Scope | Sign-in | Since |
|---|
payment-methods:read | Customer sign-in | 2026-09 |
cms
GET /cms/announcements
List announcements for the signed-in customer
| Scope | Sign-in | Since |
|---|
customers:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
POST /cms/announcements/{id}/hide
Hide an announcement for this customer
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /cms/announcements/{id}/read
Mark an announcement as read
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
GET /cms/blog-categories
List blog categories
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /cms/blog-tags
List blog tags
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /cms/pages/{locale}/{slug}
Read a published CMS page
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | path | string | Yes | |
slug | path | string | Yes | |
GET /cms/post-tags
List blog post tags
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
limit | query | string | | How many tags, most used first. |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /cms/posts
List published blog posts
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
tag | query | string | | Tag slug. |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /cms/posts/{locale}/{slug}
Read a published blog post
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | path | string | Yes | |
slug | path | string | Yes | |
domains
GET /domains/search
Check domain availability and price
| Scope | Sign-in | Since |
|---|
domains:search | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
domain | query | string | Yes | The name to look up, with its extension. |
years | query | string | | Registration years to price. |
suggestions | query | true / false | | false skips the other-extension suggestions (default true). |
knowledgebase
GET /knowledgebase
Read published knowledgebase articles
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
GET /knowledgebase/{slug}
Read published knowledgebase articles
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
slug | path | string | Yes | |
GET /knowledgebase/suggest
Read published knowledgebase articles
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
q | query | string | | What the visitor has typed so far. |
orders
GET /orders/{id}
Read one order
| Scope | Sign-in | Since |
|---|
orders:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /orders/{id}/pay
Pay for an order
| Scope | Sign-in | Since |
|---|
payments:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
cancelUrl | body | string | | Where the buyer ends up after a cancelled payment. Same rule as returnUrl. |
iban | body | string | | |
method | body | CREDIT_CARD / PAYPAL / SEPA / CRYPTO / BANK_TRANSFER / IDEAL | Yes | |
paymentMethodId | body | string | Yes | |
returnUrl | body | string | | Where the buyer ends up after the payment is confirmed, with order and result added. Must be a verified host of the store or an origin of the key. |
GET /orders/{id}/status
Read a checkout's progress with its status token
| Scope | Sign-in | Since |
|---|
checkout:write | Key only | 2026-10 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
token | query | string | Yes | The statusToken that checkout answered with. |
POST /orders/checkout
Place an order (guest or signed-in)
| Scope | Sign-in | Since |
|---|
checkout:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
currency | body | string | | ISO 4217 code the buyer is charged in; an unserved one falls back to the store's main currency. |
customer | body | object | Yes | |
customer.address | body | object | | |
customer.companyName | body | string | | |
customer.countryCode | body | string | | |
customer.customerType | body | INDIVIDUAL / BUSINESS | | |
customer.email | body | string email | Yes | |
customer.name | body | string | Yes | |
customer.password | body | string | | For a new account. |
customer.phone | body | string | | |
customer.vatId | body | string | | |
items | body | object[] | Yes | |
items[].addOnIds | body | string[] | | Add-ons, one of each. addOns wins when both are sent. |
items[].addOns | body | object[] | | Add-ons with how many of each. |
items[].addOns[].addOnId | body | string | | |
items[].addOns[].quantity | body | integer | | |
items[].configuration | body | object | | Values for the product's order fields. |
items[].domainName | body | string | | For a domain line: the name to register or transfer. |
items[].productId | body | string | | |
items[].productPriceId | body | string | | The price (billing cycle) to buy; the cheapest is used when absent. |
items[].quantity | body | integer | | |
locale | body | string | | Language code, e.g. en or de. Names and messages come back in it. |
POST /orders/preview
Price an order server-side before checkout
| Scope | Sign-in | Since |
|---|
checkout:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
currency | body | string | | ISO 4217 code the buyer is charged in; an unserved one falls back to the store's main currency. |
customer | body | object | | Only what a price depends on. |
customer.countryCode | body | string | | |
customer.customerType | body | INDIVIDUAL / BUSINESS | | |
customer.vatId | body | string | | |
items | body | object[] | Yes | |
items[].addOnIds | body | string[] | | Add-ons, one of each. addOns wins when both are sent. |
items[].addOns | body | object[] | | Add-ons with how many of each. |
items[].addOns[].addOnId | body | string | | |
items[].addOns[].quantity | body | integer | | |
items[].configuration | body | object | | Values for the product's order fields. |
items[].domainName | body | string | | For a domain line: the name to register or transfer. |
items[].productId | body | string | | |
items[].productPriceId | body | string | | The price (billing cycle) to buy; the cheapest is used when absent. |
items[].quantity | body | integer | | |
locale | body | string | | Language code, e.g. en or de. Names and messages come back in it. |
Check whether a system name can be claimed
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-10 |
| Name | In | Type | Required | Notes |
|---|
name | query | string | | The system name to check, as typed. |
List the Teculiar plans with their prices, limits and features
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-10 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
products
GET /products
List products
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-09 |
services
GET /services
List the customer's services
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
GET /services/{id}
Read one service, including its domain records
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
GET /services/{id}/actions
List the module actions a service offers
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
GET /services/{id}/actions/{action}
Run a read action of a service's module
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
action | path | string | Yes | |
POST /services/{id}/actions/{action}
Run an action of a service's module
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
action | path | string | Yes | |
PATCH /services/{id}/auto-renew
Turn a service's auto-renew on or off
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
autoRenew | body | boolean | Yes | |
POST /services/{id}/cancel
Request cancellation of a service
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
cancelDomains | body | boolean | | Also cancel the domains on the service. |
reason | body | string | | |
reasonText | body | string | | |
timing | body | string | | When the cancellation takes effect. |
GET /services/{id}/cancellation-request
The cancellation request waiting on a service, if any
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /services/{id}/change-plan
Change a service's plan
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
productPriceId | body | string | | The price to move to, from plan-options. |
POST /services/{id}/domain-actions/{action}
Run an action of a domain's registrar module
| Scope | Sign-in | Since |
|---|
domains:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
action | path | string | Yes | |
GET /services/{id}/domain-lock
Read the transfer lock on a domain service
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /services/{id}/domain-lock
Lock or unlock a domain for transfer
| Scope | Sign-in | Since |
|---|
domains:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
locked | body | boolean | Yes | |
GET /services/{id}/plan-options
Plans this service can change to
| Scope | Sign-in | Since |
|---|
services:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /services/{id}/renew-domain
Renew the domain attached to a service
| Scope | Sign-in | Since |
|---|
domains:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
years | body | integer | Yes | |
POST /services/{id}/restart
Restart a service
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /services/{id}/resume
Withdraw a cancellation request, or undo a scheduled cancellation
| Scope | Sign-in | Since |
|---|
services:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
GET /services/{id}/transfer-code
Read the transfer (EPP) code for an unlocked domain
| Scope | Sign-in | Since |
|---|
domains:manage | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
storefront
GET /storefront/domain-prices
List TLD registration and transfer prices
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-09 |
GET /storefront/embed
Read the embed origin, feature toggles and brand theme for this tenant
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
brand | query | string | | Used only where the store has saved no value of its own. |
accent | query | string | | Used only where the store has saved no value of its own. |
onAccent | query | string | | Used only where the store has saved no value of its own. |
background | query | string | | Used only where the store has saved no value of its own. |
surface | query | string | | Used only where the store has saved no value of its own. |
text | query | string | | Used only where the store has saved no value of its own. |
muted | query | string | | Used only where the store has saved no value of its own. |
line | query | string | | Used only where the store has saved no value of its own. |
danger | query | string | | Used only where the store has saved no value of its own. |
success | query | string | | Used only where the store has saved no value of its own. |
radius | query | string | | Used only where the store has saved no value of its own. |
font | query | string | | Used only where the store has saved no value of its own. |
density | query | string | | Used only where the store has saved no value of its own. |
scheme | query | string | | Used only where the store has saved no value of its own. |
logoUrl | query | string | | Used only where the store has saved no value of its own. |
GET /storefront/i18n
Languages this store serves, plus the packs installed on the platform
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-09 |
POST /storefront/inquiries
Submit a public contact enquiry
| Scope | Sign-in | Since |
|---|
inquiries:write | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
_honey | body | string | | A hidden trap field; leave it empty. A filled one is dropped as spam. |
email | body | string email | Yes | |
message | body | string | Yes | |
name | body | string | Yes | |
phone | body | string | | |
source | body | contact / inquiry | | |
subject | body | string | Yes | |
GET /storefront/modules
List the platform's module library
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-10 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /storefront/modules/{name}
Read one module with its guide
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-10 |
| Name | In | Type | Required | Notes |
|---|
name | path | string | Yes | |
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
GET /storefront/page/{key}
Read a published Customizer page by key
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
key | path | string | Yes | |
GET /storefront/payment-gateways
List enabled payment methods; credentials are never exposed
| Scope | Sign-in | Since |
|---|
payment-methods:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
locale | query | string | | Language code, e.g. en or de. Names and messages come back in it. |
currency | query | string | | ISO 4217 code the buyer is charged in; an unserved one falls back to the store's main currency. |
baseTotalCents | query | string | | The basket total in the store's main currency, for method rules. |
country | query | string | | The buyer's country code, for method rules. |
families | query | string | | Comma list of the basket's product families, for method rules. |
GET /storefront/products
List active products, optionally by category
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
category | query | string | | Category slug. |
GET /storefront/redirects
List the storefront's configured URL redirects
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
GET /storefront/settings
Read public checkout settings including VAT percent
| Scope | Sign-in | Since |
|---|
catalog:read | Key only | 2026-09 |
GET /storefront/theme
Read the active theme, menus and footer for rendering
| Scope | Sign-in | Since |
|---|
content:read | Key only | 2026-09 |
tickets
GET /tickets
List the customer's tickets
| Scope | Sign-in | Since |
|---|
tickets:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
status | query | string | | |
department | query | string | | Department id. |
POST /tickets
Open a support ticket
| Scope | Sign-in | Since |
|---|
tickets:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
Idempotency-Key | header | string | | A unique value per intended request (1 to 200 of A-Z a-z 0-9 - _). A retry with the same key and body returns the first answer instead of repeating the work. |
body | body | string | Yes | |
departmentId | body | string | Yes | |
paid | body | boolean | | An URGENT ticket needs a paid ticket credit: without paid, it is refused. |
priority | body | LOW / NORMAL / HIGH / URGENT | | |
serviceId | body | string | | |
subject | body | string | Yes | |
GET /tickets/{id}
Read one ticket with its replies
| Scope | Sign-in | Since |
|---|
tickets:read | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /tickets/{id}/attachments
Attach files to a ticket or reply
| Scope | Sign-in | Since |
|---|
tickets:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /tickets/{id}/close
Close a ticket
| Scope | Sign-in | Since |
|---|
tickets:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
POST /tickets/{id}/replies
Reply to a ticket
| Scope | Sign-in | Since |
|---|
tickets:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
id | path | string | Yes | |
body | body | string | Yes | |
internal | body | boolean | | Staff notes only; a customer who sets it is refused. |
GET /tickets/departments
List the departments a ticket can be opened against
| Scope | Sign-in | Since |
|---|
tickets:read | Customer sign-in | 2026-09 |
users
GET /users/me
Read the signed-in customer's profile
| Scope | Sign-in | Since |
|---|
customers:read | Customer sign-in | 2026-09 |
PATCH /users/me
Update the signed-in customer's profile
| Scope | Sign-in | Since |
|---|
customers:write | Customer sign-in | 2026-09 |
| Name | In | Type | Required | Notes |
|---|
address | body | object | | |
countryCode | body | string | | |
currency | body | string | | The currency the customer is billed in; must be one the store lists. |
customerType | body | INDIVIDUAL / BUSINESS | | |
email | body | string email | | |
locale | body | string | | |
name | body | string | | |
phone | body | string | | |
vatId | body | string | | |
GET /users/me/export
Export everything this account holds
| Scope | Sign-in | Since |
|---|
customers:read | Customer sign-in | 2026-09 |
Generated from version e7d170d2d on 2026-10-18.