REST API

Authenticate with a bearer token, choose abilities, handle errors and rate limits, and call the main endpoint groups with curl.

The REST API lets scripts and your own storefront do what the admin screens and client area do: list invoices, place orders, create clients, record usage and more. This article covers how to authenticate, what the limits are, and a task-by-task tour. The complete list of endpoints and fields is the OpenAPI 3.1 file your installation serves.

In the examples, $API is your base URL and $TOKEN is a token:

API=https://billing.example.com/api/v1

The OpenAPI file#

GET /api/v1/openapi.json returns the machine-readable spec, with no token needed. Import it into Postman, Insomnia or a client generator. API & webhooks in the admin menu also has an OpenAPI spec button. The file is generated from the real routes, so it lists every endpoint, query filter and request field.

Two kinds of token#

The token owner's role decides which half of the API a token can use.

Token owner Uses Notes
Customer /me, /services, /invoices, /quotes, /payments, /orders, /payment-methods, /tickets Everything is limited to the owner's own client. Another client's record answers 404.
Staff (administrator or staff) /admin/* Deleting clients, products, product groups, servers and tax rates needs an administrator, as does everything under settings, API tokens, plugin changes, webhooks and staff users.

A staff token calling a client endpoint gets 403, and so does a customer token calling /admin/*. Four things need no token: GET /products, GET /products/{id-or-slug}, GET /openapi.json and the two sign-in calls under /auth.

Get a token#

There are two ways.

In the browser. Administrators open API & webhooks → API tokens, fill in Name, choose Access (full access, or only the abilities you tick) and optionally Expires after in days, then press Create token. Copy the token straight away: it is shown once and only a hash is stored. Customers do the same on API access in the client area.

Through the API. Exchange an email and password for a token:

curl -X POST $API/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"your-password","name":"My storefront","abilities":["invoices.read","services.read"],"expires_in_days":90}'

The response (201) holds data.token, abilities, expires_at and the user. name, abilities and expires_in_days (1 to 3650) are optional. Leave out abilities to get a token that can do everything its owner can. Send the token on every later call:

curl $API/invoices -H "Authorization: Bearer $TOKEN"

DELETE /auth/token revokes the token that made the request. POST /auth/register creates a customer account and returns a token while registration is open; otherwise it answers 403 registration_closed.

Two-factor sign-in#

If the account uses two-factor authentication, add otp to the token request: the six-digit code or an unused recovery code, sent as a string ("012345").

Situation Response
No otp sent 401 otp_required (not counted as an attempt)
Wrong, already used or replayed code 401 invalid_otp
Five wrong codes in five minutes for the same user and IP 429 too_many_requests with Retry-After
Wrong password 401 invalid_credentials

The code is only asked for when the token is issued. After that the token works until it expires or is revoked, so give short-lived integrations an expires_in_days. If Settings → Security → Require two-factor authentication for staff is on, a staff token whose owner has not set up two-factor gets 403 two_factor_enrolment_required on every call (setting it up is only possible in the admin area). See Two-factor sign-in.

Abilities#

An ability is a permission written as <resource>.read for GET calls and <resource>.write for changes. A token with an ability list can only do what the list names; anything else is 403 forbidden. Unknown abilities are refused with 422 when you create the token. GET /admin/abilities returns the full list with descriptions.

Customer abilities: profile.read, profile.write, orders.write, services.read, services.write, invoices.read, invoices.pay, quotes.read, quotes.write, payment-methods.read, payment-methods.write, usage.read, tickets.read, tickets.write.

Staff abilities come in read and write pairs for these resources: clients, products, services, invoices, quotes, payments, tax-rates, servers, settings, plugins, webhooks, api-tokens, payment-methods, usage, staff-users, tickets and support. A few stand alone: dashboard.read, autopay.read, activity.read and impersonation.write.

Give each integration its own token with only the abilities it needs. A token can only grant abilities it has itself, and a token with a limited list cannot create a full-access token.

Conventions#

  • JSON in, JSON out.
  • Money is an object: {"amount": 1500, "currency": "USD", "formatted": "$15.00"}. amount is in minor units (cents). Send amounts to write endpoints as integers in minor units.
  • Dates are ISO 8601, for example 2026-11-09 or 2026-11-09T10:00:00+00:00.
  • Customer endpoints address services, invoices and quotes by uuid. Admin endpoints use the numeric id.
  • Secrets are write-only. Server passwords, gateway keys, the SMTP password, plugin secrets and token values are never returned by a read call; the response says whether one is stored.
  • Every change made through the admin API is written to the activity log (GET /admin/activity) marked as coming from the API.

Pagination and filters#

List endpoints return {"data": [...], "links": {...}, "meta": {...}}. Use ?page=2&per_page=50. per_page defaults to 25 and is capped at 100.

Filters are query parameters, for example status, client_id and q; the spec lists them per endpoint. An unknown status is a 422, not an empty list. Date filters (from, to) are inclusive and use YYYY-MM-DD.

curl "$API/admin/invoices?status=overdue&client_id=12&per_page=50" \
  -H "Authorization: Bearer $TOKEN"

Rate limits#

  • 120 requests per minute per token (per IP address for the public product calls).
  • 10 requests per minute per IP address on /auth/token and /auth/register.

Going over returns 429 with a Retry-After header giving the seconds to wait.

Errors#

Errors share one shape:

{"error": {"code": "validation_failed", "message": "The given data was invalid.", "details": {"email": ["The email field is required."]}}}
HTTP code Meaning
401 unauthenticated Token missing, wrong, expired or its owner is inactive
401 invalid_credentials, otp_required, invalid_otp Sign-in problems, see above
403 forbidden Wrong kind of token, missing ability, or administrator needed
404 not_found No such record, or it belongs to another client
422 validation_failed Bad input; details lists the fields
422 domain_error A business rule, such as "only draft invoices can be edited"
422 action_failed A service module action reported failure
429 too_many_requests Rate limit; see Retry-After
500 server_error Something failed on the server

Tour: customer endpoints#

Show the catalogue (no token):

curl $API/products

Order a product. This creates a pending service and its invoice and returns a pay_url. If the product has a free trial the customer has not used, the service is provisioned instead and invoice and pay_url are null.

curl -X POST $API/orders -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"product_id": 1, "billing_cycle": "monthly"}'

Pay an invoice. GET /gateways lists the payment methods. POST /invoices/{uuid}/pay with the gateway key returns a checkout_url to send the customer to. The gateway confirms by its own webhook, the invoice becomes paid and the service is provisioned. Poll GET /services/{uuid} or subscribe to a webhook.

curl -X POST $API/invoices/INVOICE_UUID/pay -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"gateway": "stripe"}'

Saved payment methods. POST /payment-methods/setup returns a provider-hosted page for the customer to add a card; POST /invoices/{uuid}/pay-saved charges one. Provider tokens are never returned.

Other customer calls: GET /me, PATCH /me, GET /services, POST /services/{uuid}/cancel, plan changes under /services/{uuid}/plan-options and /plan-change, GET /quotes with accept and decline, GET /invoices/{uuid}/pdf, GET /services/{uuid}/usage, and /tickets while support tickets are switched on.

Tour: admin endpoints#

Admin calls start with /admin. GET /admin/me shows the staff user behind the token and its abilities.

Create a client (add password to also create a portal login):

curl -X POST $API/admin/clients -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Jane Example","email":"[email protected]","company_name":"Example Ltd","country":"US"}'

Create a service for a client. By default this places an order, like the customer endpoint. Send "create_invoice": false to create only the service.

curl -X POST $API/admin/services -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"client_id": 12, "product_id": 1, "billing_cycle": "monthly", "label": "example.com"}'

Create and issue an invoice. items needs description and unit_price (minor units); quantity and taxable are optional. issue: true issues it straight away.

curl -X POST $API/admin/invoices -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"client_id": 12, "items": [{"description": "Setup", "unit_price": 5000, "quantity": 1}], "issue": true}'

Record a manual payment (bank transfer, cash, cheque), then refund a payment:

curl -X POST $API/admin/invoices/34/payments -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"amount": 5000, "method": "bank transfer"}'

curl -X POST $API/admin/payments/56/refund -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"amount": 1000, "reason": "Goodwill"}'

Leave out amount on a refund to refund what is left.

Service lifecycle. POST /admin/services/{id}/activate, suspend, unsuspend, terminate and cancel. suspend takes an optional reason.

Record metered usage. The idempotency_key makes retries safe: a repeat returns the original record with 200 and fires no event.

curl -X POST $API/admin/services/78/usage -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"meter":"api_calls","quantity":120,"idempotency_key":"job-8841","source":"myapp"}'

See Usage-based billing.

Webhooks. POST /admin/webhooks (administrators only) creates an endpoint and returns its signing secret once. See Webhooks.

More groups. Dashboard, quotes, product groups, tax rates, servers (POST /admin/servers/{id}/test), settings sections (GET and PUT /admin/settings/{section}), plugins (list, enable, disable, settings), API tokens, staff users, support tickets and departments, and the activity log. Settings updates are partial: only the keys you send change, null resets a key to its default, a blank secret keeps the stored one, and unknown keys are a 422.

Plugin upload and installation are deliberately not in the API, because a plugin is code that runs on your server. Install plugins in Plugins or with php artisan plugin install. See Plugins.

PDFs#

GET /invoices/{uuid}/pdf and GET /quotes/{uuid}/pdf return application/pdf as a download. Add ?inline=1 to display instead. Admin equivalents are /admin/invoices/{id}/pdf and /admin/quotes/{id}/pdf, which include drafts.

Was this article helpful?
Last updated 10 Nov 2026
Powered by Tidemark Billing