API Reference
Complete reference for the ABAXUS billing API. Every operation is a REST call. Every number is DECIMAL(20,10) — never float. Every write endpoint is idempotent.
Overview
ABAXUS is a self-hosted, API-first usage-based billing engine. You deploy it inside your infrastructure; your application calls it over HTTP. There is no external dependency beyond your chosen payment provider (Stripe is the default).
https://api.your-domain.comapplication/jsonDECIMAL(20,10)Authentication
All requests require a Bearer token in the Authorization header. API keys are generated in the ABAXUS admin dashboard. Keys never expire but can be rotated at any time.
Authorization: Bearer abx_live_xxxxxxxxxxxxx Content-Type: application/json
curl -X POST \ -H "Authorization: Bearer abx_live_xxx" \ -H "Content-Type: application/json" \ https://api.your-domain.com/v1/events \ -d '{"customer_id":"cust_acme",...}'
abx_live_. Test keys start with abx_test_. Test keys accept all requests but do not trigger real payment charges.Idempotency
All POST and PATCH endpoints accept an idempotency_key field in the request body. Sending the same key a second time returns the original response without re-executing the operation — safe to retry on any network failure.
idempotency_keyrequired on eventsIdempotency-Keyheader altErrors
The API uses standard HTTP status codes. Error bodies always include error.code (machine-readable) and error.message (human-readable).
{
"error": {
"code": "METRIC_KEY_DUPLICATE",
"message": "A metric with key 'api_calls' already exists.",
"field": "key"
}
}A metric is the definition of a billable signal. It tells ABAXUS how to aggregate raw events into a usage total. The key is the stable identifier used throughout the system — across events, charges, and usage queries.
Define a new billable signal. The key must be unique and URL-safe — it is used as the stable identifier in all downstream references.
keyrequireddisplay_namerequiredaggregation_typerequiredsum · count · max · min · last · unique_count · percentilevalue_typeoptionalinteger (default) or decimal. Determines allowed event value format.filtersoptional{
"key": "api_calls",
"display_name": "API Calls",
"aggregation_type": "sum",
"value_type": "integer",
"filters": ["region", "tier"]
}{
"key": "api_calls",
"display_name": "API Calls",
"aggregation_type": "sum",
"value_type": "integer",
"active": true,
"created_at": "2026-03-17T10:00:00Z"
}activeoptionallimitoptionalcursoroptionalmeta.next_cursor for pagination.{
"data": [ { "key": "api_calls", ... } ],
"meta": {
"total": 12,
"next_cursor": "eyJrZXkiOiJzdG9y..."
}
}Updates display_name and filters. The key and aggregation_type are immutable after creation to preserve historical correctness.
Soft-deactivates the metric — sets active: false. Existing events and historical aggregates are preserved. New events using this key will be rejected with 422.
A price plan contains one or more charges, each linked to a metric and a pricing model. Plans are immutable — creating a plan with an existing id creates a new version. Active subscriptions always pin to their original plan version.
idrequiredid creates a new version.namerequiredcurrencyrequiredUSD).chargesrequired| per_unit | quantity × unit_amount |
| tiered | Each tier billed independently |
| volume | All units at the tier the total falls in |
| package | Rounds up to whole packages of package_size |
| flat_fee | Fixed amount regardless of usage |
{
"id": "plan_growth",
"name": "Growth",
"currency": "USD",
"charges": [
{
"key": "api_charge",
"metric_key": "api_calls",
"model": "tiered",
"properties": {
"tiers": [
{ "up_to": 10000, "unit_amount": "0.001" },
{ "up_to": null, "unit_amount": "0.0005" }
]
}
},
{
"key": "seat_fee",
"metric_key": "seats",
"model": "flat_fee",
"properties": { "amount": "49.00" }
}
]
}{
"id": "plan_growth",
"version": 1,
"name": "Growth",
"currency": "USD",
"charges": [ /* ... */ ],
"created_at": "2026-03-17T10:00:00Z"
}Returns the latest version of each plan. Supports ?active=true filter and cursor pagination.
Returns the full version history for a plan, oldest first. Each version is a complete snapshot of charges as they were when created — historical subscriptions reference a specific version number.
A customer record holds identity, billing provider credentials, and optionally a default payment method. Customers are the billing subject — events and invoices always reference a customer ID.
provider_customer_id (Stripe's cus_xxx) alongside provider_payment_method. Stripe requires a Customer to reuse a PaymentMethod across multiple PaymentIntents.idrequirednamerequiredbillingrequiredprovider and credentials.payment_methodoptionalemailoptionalmetadataoptional{
"id": "cust_acme",
"name": "Acme Corp",
"email": "billing@acme.com",
"billing": {
"provider": "stripe",
"credentials": {
"secret_key": "sk_test_..."
}
},
"payment_method": {
"provider": "stripe",
"provider_customer_id": "cus_xxx",
"provider_payment_method": "pm_xxx",
"type": "card",
"display_last4": "4242",
"is_default": true
}
}Cursor-paginated list. Supports ?status=active filter. Response includes display fields only — payment credentials are never returned.
Returns full customer detail including payment method display fields (display_last4, type). Raw Stripe credentials are never returned.
Partial update. Mutable fields: name, email, status, metadata, and billing.credentials. The customer id is immutable.
Replaces the customer's default payment method. The new provider_payment_method (pm_xxx) must already be attached to the Stripe customer (cus_xxx) — Stripe requires this for reuse across multiple charges.
Permanently deletes the customer record. Customers with outstanding issued invoices must have those archived first.
Creates a server-side session for the Adyen Web Drop-in so the browser can tokenise a card without raw card data touching the API. The customer record does not need to exist yet — useful for new-customer forms that pre-generate a customer ID.
return_urloptionalcurrencyoptionalonPaymentCompleted, call POST /v1/customers/:id/adyen-session/complete. The API calls Adyen listRecurringDetails, fetches the stored recurringDetailReference, and upserts it as the customer's default payment method. The customer must exist before calling /complete.{
"session_id": "CS...",
"session_data": "...",
"client_key": "test_xxx",
"environment": "test"
}{
"id": "pm_...",
"provider": "adyen",
"display_brand": "visa",
"display_last4": "4242",
"is_default": true
}PUT accepts a multipart upload (PNG, JPEG, WebP — max 2 MB) and replaces any existing logo, returning the logo URL. DELETE /v1/customers/:id/logo removes the logo and returns 204 No Content.
A subscription activates a price plan for a customer. It records plan_version at creation time — locking the customer to that exact plan definition for the lifetime of the subscription.
customer_idrequiredplan_idrequiredstart_daterequiredend_dateoptional{
"customer_id": "cust_acme",
"plan_id": "plan_growth",
"start_date": "2026-01-01T00:00:00Z"
}{
"id": "sub_a1b2c3",
"customer_id": "cust_acme",
"plan_id": "plan_growth",
"plan_version": 1,
"status": "active",
"start_date": "2026-01-01T00:00:00Z"
}Supports ?customer_id= and ?status=active filters. Cursor-paginated.
An amendment is a scheduled or immediate change to an active subscription — plan upgrades, pauses, cancellations, or override changes. Applied transactionally at the correct moment by a background worker; the subscription row is never mutated directly.
| Type | Effect | Payload fields |
|---|---|---|
plan_change | Switch to a new plan version; proration invoice if immediate | new_plan_id, new_plan_version |
override_update | Modify per-subscription charge overrides | overrides (JSONB) |
pause | Set status to paused; stop billing | — |
resume | Restore status to active after pause | — |
cancel | Set status to cancelled; close current period | — |
apply_at_period_end: false (default) applies at effective_at. apply_at_period_end: true defers to the next billing anchor — no proration. For immediate plan_change, the system closes the current period at effective_at with the old plan (partial-period invoice) and opens a new period under the new plan.typerequiredeffective_atoptionalapply_at_period_endoptionalfalse. Set true to defer to the billing period end.new_plan_idplan_change onlynew_plan_versionplan_change only{
"type": "plan_change",
"effective_at": "2026-05-01T00:00:00Z",
"apply_at_period_end": false,
"new_plan_id": "plan_pro",
"new_plan_version": "4"
}{
"id": "amd_01J...",
"subscription_id": "sub_01J...",
"type": "plan_change",
"status": "scheduled",
"effective_at": "2026-05-01T00:00:00Z",
"apply_at_period_end": false,
"payload": {
"new_plan_id": "plan_pro",
"new_plan_version": "4"
},
"created_at": "2026-04-03T10:00:00Z"
}Returns all amendments in descending created_at order. Filter by ?status=scheduled|applied|cancelled.
Returns 204 No Content. Returns 409 Conflict if the amendment has already been applied. Only scheduled amendments can be cancelled.
Events are the raw usage signal. Each event records a quantity of a metric at a point in time. idempotency_key is mandatory — duplicate keys are silently skipped, making retries safe on any network failure.
Queued asynchronously — never blocks your request path. Returns 202 Accepted immediately. The event is guaranteed to be durably stored before the response is returned.
customer_idrequiredmetric_keyrequiredvaluerequiredvalue_type.idempotency_keyrequiredtimestampoptionalsubscription_idoptionalpropertiesoptionalfilters can be used in usage queries.{
"customer_id": "cust_acme",
"subscription_id": "sub_a1b2c3",
"metric_key": "api_calls",
"value": "1",
"timestamp": "2026-03-17T14:00:00Z",
"idempotency_key": "evt_acme_req_abc123",
"properties": {
"region": "us-east-1"
}
}{
"id": "evt_xyz",
"status": "accepted",
"idempotency_key": "evt_acme_req_abc123"
}Ingest up to 500 events in a single request. Returns 207 Multi-Status — each item has its own status. One invalid event never rejects the rest.
{
"events": [
{ "idempotency_key": "evt_001", /* ... */ },
{ "idempotency_key": "evt_002", /* ... */ }
]
}
// Response: 207
{
"results": [
{ "idempotency_key": "evt_001", "status": 202 },
{ "idempotency_key": "evt_002", "status": 202 }
]
}Bypasses the async queue and writes directly to the events table. Automatically invalidates all pre-computed aggregates that overlap the backfilled time range. Use for data migrations or corrections.
Cursor-paginated. Supports filters: customer_id, metric_key, subscription_id, from, to (ISO 8601).
Returns bucketed event counts. Query params: customer_id, metric_key, from, to, bucket=day|week|month.
Two endpoints with different consistency guarantees. Use summary for dashboards and customer-facing widgets. Use compute when you need the authoritative figure that will appear on an invoice.
Reads from pre-computed aggregates. May lag by meta.lag_seconds. The response includes meta.consistency: "eventual". Best for high-frequency dashboard polling.
customer_idrequiredmetric_keyrequiredperiod_startrequiredperiod_endrequired# Query params ?customer_id=cust_acme &metric_key=api_calls &period_start=2026-03-01T00:00:00Z &period_end=2026-04-01T00:00:00Z // 200 Response { "value": "85000", "metric_key": "api_calls", "meta": { "consistency": "eventual", "lag_seconds": 4 } }
Real-time scan of the raw events table. Returns meta.consistency: "exact". This is the value used internally when billing is calculated. Idempotent.
{
"customer_id": "cust_acme",
"metric_key": "api_calls",
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-04-01T00:00:00Z",
"idempotency_key":"usage_acme_2026-03"
}
// 200 Response
{
"value": "85000",
"meta": { "consistency": "exact" }
}Run a calculation to see the full line-item breakdown before invoicing. The calculation_id is stored in the database and referenced inside the resulting invoice for a full audit trail.
Computes the exact amount owed for a subscription period. Uses usage/compute internally for each metric. The result is idempotent per idempotency_key.
customer_idrequiredsubscription_idrequiredperiod_startrequiredperiod_endrequiredidempotency_keyoptional{
"customer_id": "cust_acme",
"subscription_id": "sub_a1b2c3",
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-04-01T00:00:00Z",
"idempotency_key": "calc_acme_2026-03"
}{
"calculation_id": "calc_xyz",
"total_amount": "127.50",
"currency": "USD",
"line_items": [
{
"charge_key": "api_charge",
"model": "tiered",
"quantity": "85000",
"amount": "52.50",
"tiers": [
{ "up_to": 10000, "quantity": 10000, "amount": "10.00" },
{ "up_to": null, "quantity": 75000, "amount": "37.50" }
]
},
{ "charge_key": "seat_fee", "amount": "75.00" }
]
}Calculates a hypothetical cost without a subscription. Pass a plan_id and a list of {metric_key, value} pairs. Useful for pricing page calculators and quote tools.
{
"plan_id": "plan_growth",
"usage": [
{ "metric_key": "api_calls", "value": "50000" },
{ "metric_key": "seats", "value": "1" }
]
}Invoices capture a billing period's calculated amount and move through a well-defined lifecycle: issued → paid or archived. Invoices with a $0 total are rejected. Re-running with the same effective period is idempotent.
cutoff_date becomes period_end. period_start is derived from the date of the last invoice or the earliest subscription start_date — whichever is later. The system runs pricing calculation internally.
customer_idrequiredsubscription_idsoptionalcutoff_daterequired{
"customer_id": "cust_acme",
"subscription_ids": ["sub_a1b2c3"],
"cutoff_date": "2026-03-31T23:59:59Z"
}{
"id": "inv_abc",
"customer_id": "cust_acme",
"status": "issued",
"total_amount": "127.50",
"currency": "USD",
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-03-31T23:59:59Z",
"line_items": [ /* ... */ ],
"issued_at": "2026-04-01T00:00:00Z"
}Creates invoices for all active customers in a single call. Accepts cutoff_date only. Each customer gets their own invoice. Customers with $0 total are skipped. Returns a job ID — poll GET /v1/invoices/bulk/:job_id for status.
Cursor-paginated. Supports filters: customer_id, status, from, to.
{
"events": [
{ "type": "issued", "at": "2026-04-01T00:00:00Z" },
{ "type": "email_sent", "at": "2026-04-01T00:01:00Z" },
{ "type": "charged", "at": "2026-04-01T00:01:45Z",
"amount": "127.50", "payment_intent": "pi_xxx" }
]
}Dispatches the invoice email to customer.email. Only valid from issued status. Idempotent — re-sending returns a 200 without dispatching a duplicate.
Creates a Stripe PaymentIntent for the invoice total using the customer's default payment method. On success, transitions the invoice to paid and records the payment_intent ID on the audit trail. Idempotent.
Moves the invoice to archived status. Valid from both issued and paid. Archived invoices are preserved in full for audit purposes and can never be charged again.
Entitlements define what a customer is permitted to do. Price plans define how much to charge. They are queried and enforced independently but are always in sync — entitlement definitions are declared on the same plan version as charges, and a subscription's locked version covers both.
boolean → OR across subscriptions; limit → SUM; custom → latest plan version wins. Resolution is computed at query time — no denormalised table is maintained.| Type | value shape | Use case |
|---|---|---|
boolean | true / false | Feature flags — SSO, priority support, custom branding |
limit | integer or decimal | Numeric caps — max seats, max projects, monthly API calls. Set metric_key for automatic usage comparison. |
custom | any JSON object | Structured config — rate limit objects, allowlists, tier labels |
Entitlements are declared inside POST /v1/price-plans under an entitlements key alongside charges. They are immutable once the plan version is published — add or change entitlements by publishing a new plan version.
Walks the customer's active subscriptions, merges all entitlement definitions by the rules above, and returns the resolved set. current_usage and remaining are populated only when metric_key is set on the definition — consistency is eventual (same as GET /v1/usage/summary).
{
"customer_id": "cust_acme",
"resolved_at": "2026-04-03T12:00:00Z",
"sources": ["sub_xxx", "sub_yyy"],
"entitlements": [
{
"feature_key": "sso",
"type": "boolean",
"granted": true
},
{
"feature_key": "max_api_calls",
"type": "limit",
"granted": true,
"value": 1000000,
"current_usage": 423817,
"remaining": 576183,
"exceeded": false,
"metric_key": "api_calls"
},
{
"feature_key": "rate_limit",
"type": "custom",
"granted": true,
"value": { "rpm": 1000, "burst": 200 }
}
]
}Designed to be called on every inbound request for enforcement. Returns a minimal payload for fast parsing. Query params: customer_id and feature_key.
granted: true, exceeded: true means the feature exists on the plan but the limit is over — callers can distinguish "not on plan" from "on plan but over limit" and implement different policies (hard block, soft overage, grace period)./check queries usage_aggregates on every call — fast but not free.{
"customer_id": "cust_acme",
"feature_key": "max_api_calls",
"granted": true,
"type": "limit",
"value": 1000000,
"current_usage": 423817,
"remaining": 576183,
"exceeded": false
}{
"customer_id": "cust_acme",
"feature_key": "sso",
"granted": false,
"type": "boolean",
"value": false
}Returns the raw entitlement_definitions rows for the specified (plan_id, plan_version) pair — useful for building plan comparison UI or auditing what a specific subscription version grants.
Price plans are immutable by design — every change creates a new version so historical subscriptions are never broken. Three optional metadata fields (effective_from, changelog, deprecated_at) make versioning operationally useful without changing the core model.
| Field | Purpose |
|---|---|
effective_from | Schedule a future version to become the default for new subscriptions automatically. null = available but not yet promoted. |
changelog | Free-text description of what changed — shown in the UI version history view. |
deprecated_at | Prevents new subscriptions from referencing this version. Existing subscriptions are honoured until amended. |
versionrequired"3").forceoptionaldeprecated_at to block new ones.{
"version": "3",
"force": false
}{
"id": "plan_growth",
"version": "3",
"deprecated_at": "2026-04-03T10:00:00Z"
}Charge credentials are read from the global Settings → Integrations → Payment Processing configuration, not from per-customer records. Rotating API keys takes effect immediately for all customers — there is nothing to update on individual customer records.
Stripe
ABAXUS creates a Stripe PaymentIntent when POST /v1/invoices/:id/charge is called. Card tokenisation uses Stripe.js / Stripe Elements in the browser — raw card numbers never reach the API.
provider_customer_id (cus_xxx) alongside provider_payment_method when creating or updating a Stripe payment method. Stripe requires the PaymentMethod to be attached to a Customer for reuse — without it, the second charge will fail with HTTP 400.Stripe PaymentIntents APIpm_xxx (Stripe PaymentMethod ID)Adyen
Adyen uses a server-assisted session flow so the browser Drop-in can store cards without raw card data touching the API. Payment methods are tokenised through the Adyen Drop-in UI component.
Adyen Checkout /payments (stored method)recurringDetailReferencelistRecurringDetails uses pal-live.adyen.com. Configure live_url_prefix in Settings → Payments so checkout API calls route to the correct live region.Adyen Drop-in Flow
Three steps to store a card via the Adyen Drop-in and link it to a customer.
1. POST /v1/customers/:id/adyen-session ← { session_id, session_data, client_key, environment } 2. Browser mounts Adyen Web Drop-in with the session. User enters card details and submits. Adyen stores the card and fires: onPaymentCompleted({ resultCode: "Authorised" }) 3. POST /v1/customers/:id/adyen-session/complete ← { id, provider: "adyen", display_brand, display_last4, … } API calls Adyen listRecurringDetails, picks the most recent stored method, encrypts the recurringDetailReference, and upserts it as the customer's default payment method.
Ready to integrate?
See the full billing flow from a fresh deployment to a paid invoice — with code at every step.