v1 · REST · JSON base: https://api.your-domain.com

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).

Base URL
https://api.your-domain.com
Format
application/json
Precision
DECIMAL(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.

Request headers
Authorization: Bearer abx_live_xxxxxxxxxxxxx
Content-Type:  application/json
cURL example
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",...}'
Key format: Production keys start with 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.

Field
Behaviour
idempotency_keyrequired on events
Any unique string (max 255 chars). UUID v4 recommended. Events require this field; all other endpoints accept it as optional but strongly recommended.
Idempotency-Keyheader alt
Can also be passed as an HTTP header instead of in the body. Body field takes precedence if both are present.

Errors

The API uses standard HTTP status codes. Error bodies always include error.code (machine-readable) and error.message (human-readable).

200OK — synchronous success
201Created — resource created
202Accepted — async (events)
207Multi-Status — batch partial
400Bad Request — invalid payload
401Unauthorized — bad API key
404Not Found — resource missing
409Conflict — idempotency mismatch
422Unprocessable — business rule violation
429Rate Limited — slow down
400 Error body
{
  "error": {
    "code":    "METRIC_KEY_DUPLICATE",
    "message": "A metric with key 'api_calls' already exists.",
    "field":   "key"
  }
}
Metrics

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.

POST /v1/metrics Create a metric

Define a new billable signal. The key must be unique and URL-safe — it is used as the stable identifier in all downstream references.

Field
Type
Description
keyrequired
string
Unique slug. Lowercase, hyphens/underscores OK. Immutable after creation.
display_namerequired
string
Human-readable label shown in the dashboard and invoices.
aggregation_typerequired
enum
sum · count · max · min · last · unique_count · percentile
value_typeoptional
enum
integer (default) or decimal. Determines allowed event value format.
filtersoptional
string[]
Property keys from event payloads that can be used to filter this metric's aggregation.
POST /v1/metrics
{
  "key":              "api_calls",
  "display_name":     "API Calls",
  "aggregation_type": "sum",
  "value_type":       "integer",
  "filters":          ["region", "tier"]
}
201 Response
{
  "key":              "api_calls",
  "display_name":     "API Calls",
  "aggregation_type": "sum",
  "value_type":       "integer",
  "active":           true,
  "created_at":       "2026-03-17T10:00:00Z"
}
GET /v1/metrics List metrics
Query param
Type
Description
activeoptional
boolean
Filter by active status. Omit to return all.
limitoptional
integer
Max results per page (default 50, max 500).
cursoroptional
string
Opaque cursor from meta.next_cursor for pagination.
GET /v1/metrics?active=true&limit=50
{
  "data": [ { "key": "api_calls", ... } ],
  "meta": {
    "total": 12,
    "next_cursor": "eyJrZXkiOiJzdG9y..."
  }
}
PATCH /v1/metrics/:key Update metric

Updates display_name and filters. The key and aggregation_type are immutable after creation to preserve historical correctness.

DELETE /v1/metrics/:key Deactivate metric

Soft-deactivates the metric — sets active: false. Existing events and historical aggregates are preserved. New events using this key will be rejected with 422.

Price Plans

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.

POST /v1/price-plans Create plan
Field
Type
Description
idrequired
string
Stable plan identifier. Re-using an existing id creates a new version.
namerequired
string
Human-readable plan name.
currencyrequired
string
ISO 4217 currency code (e.g. USD).
chargesrequired
Charge[]
Array of charges. Each charge links a metric to a pricing model.
Pricing Models
per_unitquantity × unit_amount
tieredEach tier billed independently
volumeAll units at the tier the total falls in
packageRounds up to whole packages of package_size
flat_feeFixed amount regardless of usage
POST /v1/price-plans
{
  "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" }
    }
  ]
}
201 Response
{
  "id": "plan_growth",
  "version": 1,
  "name": "Growth",
  "currency": "USD",
  "charges": [ /* ... */ ],
  "created_at": "2026-03-17T10:00:00Z"
}
GET /v1/price-plans List plans

Returns the latest version of each plan. Supports ?active=true filter and cursor pagination.

GET /v1/price-plans/:id/versions List all versions of a plan

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.

Customers

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.

POST /v1/customers Create a customer
Stripe requirement: Always include provider_customer_id (Stripe's cus_xxx) alongside provider_payment_method. Stripe requires a Customer to reuse a PaymentMethod across multiple PaymentIntents.
Field
Type
Description
idrequired
string
Your stable customer identifier.
namerequired
string
Display name for dashboards and invoices.
billingrequired
object
Payment provider config. Contains provider and credentials.
payment_methodoptional
object
Default payment method. See schema below.
emailoptional
string
Billing email address. Used when sending invoice emails.
metadataoptional
object
Arbitrary key-value pairs. Returned on every response but never used in billing logic.
POST /v1/customers
{
  "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
  }
}
GET /v1/customers List customers

Cursor-paginated list. Supports ?status=active filter. Response includes display fields only — payment credentials are never returned.

GET /v1/customers/:id Retrieve a customer

Returns full customer detail including payment method display fields (display_last4, type). Raw Stripe credentials are never returned.

PATCH /v1/customers/:id Update a customer

Partial update. Mutable fields: name, email, status, metadata, and billing.credentials. The customer id is immutable.

POST /v1/customers/:id/payment-methods Replace payment method (Stripe)

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.

DELETE /v1/customers/:id Delete a customer

Permanently deletes the customer record. Customers with outstanding issued invoices must have those archived first.

POST /v1/customers/:id/adyen-session Create Adyen Drop-in session

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.

Query param
Default
Description
return_urloptional
https://localhost
Redirect URL for redirect-based payment methods.
currencyoptional
USD
ISO 4217 currency code for the zero-auth session.
Completing the flow: After the Drop-in fires onPaymentCompleted, 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.
200 Session response
{
  "session_id":   "CS...",
  "session_data": "...",
  "client_key":   "test_xxx",
  "environment":  "test"
}
POST /v1/customers/:id/adyen-session/complete
{
  "id":             "pm_...",
  "provider":      "adyen",
  "display_brand": "visa",
  "display_last4": "4242",
  "is_default":    true
}
Subscriptions

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.

POST /v1/subscriptions Create a subscription
Field
Type
Description
customer_idrequired
string
The customer to subscribe.
plan_idrequired
string
The price plan. The latest version is pinned at creation.
start_daterequired
string
ISO 8601 UTC datetime. The subscription is active from this date.
end_dateoptional
string
If set, the subscription auto-cancels on this date.
POST /v1/subscriptions
{
  "customer_id": "cust_acme",
  "plan_id":     "plan_growth",
  "start_date":  "2026-01-01T00:00:00Z"
}
201 Response
{
  "id":           "sub_a1b2c3",
  "customer_id": "cust_acme",
  "plan_id":     "plan_growth",
  "plan_version": 1,
  "status":      "active",
  "start_date":  "2026-01-01T00:00:00Z"
}
GET /v1/subscriptions List subscriptions

Supports ?customer_id= and ?status=active filters. Cursor-paginated.

Subscription Amendments

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.

TypeEffectPayload fields
plan_changeSwitch to a new plan version; proration invoice if immediatenew_plan_id, new_plan_version
override_updateModify per-subscription charge overridesoverrides (JSONB)
pauseSet status to paused; stop billing
resumeRestore status to active after pause
cancelSet status to cancelled; close current period
Timing: 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.
POST /v1/subscriptions/:id/amendments Schedule an amendment → 201
Field
Type
Description
typerequired
string
Amendment type — see table above.
effective_atoptional
string
ISO 8601 UTC. Defaults to now for immediate amendments.
apply_at_period_endoptional
boolean
Default false. Set true to defer to the billing period end.
new_plan_idplan_change only
string
Target plan ID.
new_plan_versionplan_change only
string
Target version. Omit to pin the current active version.
POST /v1/subscriptions/:id/amendments
{
  "type":               "plan_change",
  "effective_at":       "2026-05-01T00:00:00Z",
  "apply_at_period_end": false,
  "new_plan_id":        "plan_pro",
  "new_plan_version":   "4"
}
201 Response
{
  "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"
}
GET /v1/subscriptions/:id/amendments List amendments

Returns all amendments in descending created_at order. Filter by ?status=scheduled|applied|cancelled.

DELETE /v1/subscriptions/:id/amendments/:amendment_id Cancel a scheduled amendment

Returns 204 No Content. Returns 409 Conflict if the amendment has already been applied. Only scheduled amendments can be cancelled.

Events

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.

POST /v1/events Ingest a single event → 202

Queued asynchronously — never blocks your request path. Returns 202 Accepted immediately. The event is guaranteed to be durably stored before the response is returned.

Field
Type
Description
customer_idrequired
string
Customer this usage belongs to.
metric_keyrequired
string
Must match an active metric key.
valuerequired
string
Numeric value as a string. Must match metric's value_type.
idempotency_keyrequired
string
Unique per event. UUID v4 recommended. Duplicate keys return the original response.
timestampoptional
string
ISO 8601 UTC. Defaults to now. Use for backfilling with exact timestamps.
subscription_idoptional
string
Scopes usage to a specific subscription. Required when the customer has multiple active subscriptions.
propertiesoptional
object
Arbitrary key-value pairs. Values for keys listed in the metric's filters can be used in usage queries.
POST /v1/events → 202
{
  "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"
  }
}
202 Response
{
  "id":               "evt_xyz",
  "status":           "accepted",
  "idempotency_key": "evt_acme_req_abc123"
}
POST /v1/events/batch Batch ingest → 207

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.

POST /v1/events/batch → 207
{
  "events": [
    { "idempotency_key": "evt_001", /* ... */ },
    { "idempotency_key": "evt_002", /* ... */ }
  ]
}

// Response: 207
{
  "results": [
    { "idempotency_key": "evt_001", "status": 202 },
    { "idempotency_key": "evt_002", "status": 202 }
  ]
}
POST /v1/events/backfill Backfill historical events

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.

GET /v1/events List events

Cursor-paginated. Supports filters: customer_id, metric_key, subscription_id, from, to (ISO 8601).

GET /v1/events/histogram Event histogram

Returns bucketed event counts. Query params: customer_id, metric_key, from, to, bucket=day|week|month.

Usage

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.

GET /v1/usage/summary Eventual summary — fast

Reads from pre-computed aggregates. May lag by meta.lag_seconds. The response includes meta.consistency: "eventual". Best for high-frequency dashboard polling.

Query param
Type
Description
customer_idrequired
string
Customer to query.
metric_keyrequired
string
Metric to aggregate.
period_startrequired
string
ISO 8601 UTC start of the period.
period_endrequired
string
ISO 8601 UTC end of the period.
GET /v1/usage/summary
# 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
  }
}
POST /v1/usage/compute Exact computation — authoritative

Real-time scan of the raw events table. Returns meta.consistency: "exact". This is the value used internally when billing is calculated. Idempotent.

POST /v1/usage/compute
{
  "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" }
}
Pricing

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.

POST /v1/pricing/calculate Calculate billing

Computes the exact amount owed for a subscription period. Uses usage/compute internally for each metric. The result is idempotent per idempotency_key.

Field
Type
Description
customer_idrequired
string
The customer being billed.
subscription_idrequired
string
The subscription to calculate charges for.
period_startrequired
string
ISO 8601 UTC billing period start.
period_endrequired
string
ISO 8601 UTC billing period end.
idempotency_keyoptional
string
Recommended. Same key returns the cached calculation result.
POST /v1/pricing/calculate
{
  "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"
}
200 Response
{
  "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" }
  ]
}
POST /v1/pricing/preview Preview pricing

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.

POST /v1/pricing/preview
{
  "plan_id": "plan_growth",
  "usage": [
    { "metric_key": "api_calls", "value": "50000" },
    { "metric_key": "seats",     "value": "1" }
  ]
}
Invoices

Invoices capture a billing period's calculated amount and move through a well-defined lifecycle: issuedpaid or archived. Invoices with a $0 total are rejected. Re-running with the same effective period is idempotent.

issued → charge → paid | issued → archive → archived | paid → archive → archived
POST /v1/invoices Create an invoice

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.

Field
Type
Description
customer_idrequired
string
Customer to invoice.
subscription_idsoptional
string[]
Scope to specific subscriptions. Omit to include all active subscriptions.
cutoff_daterequired
string
ISO 8601 UTC. Usage up to and including this moment is included.
POST /v1/invoices
{
  "customer_id":      "cust_acme",
  "subscription_ids": ["sub_a1b2c3"],
  "cutoff_date":      "2026-03-31T23:59:59Z"
}
201 Response
{
  "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"
}
POST /v1/invoices/bulk Bulk create invoices

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.

GET /v1/invoices List invoices

Cursor-paginated. Supports filters: customer_id, status, from, to.

GET /v1/invoices/:id/events Audit trail
200 Audit trail response
{
  "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" }
  ]
}
POST /v1/invoices/:id/send-email Send invoice email

Dispatches the invoice email to customer.email. Only valid from issued status. Idempotent — re-sending returns a 200 without dispatching a duplicate.

POST /v1/invoices/:id/charge Charge the invoice

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.

POST /v1/invoices/:id/archive Archive invoice

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

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.

Multi-subscription resolution: A customer with multiple active subscriptions (base plan + add-on) gets a merged entitlement set: boolean → OR across subscriptions; limit → SUM; custom → latest plan version wins. Resolution is computed at query time — no denormalised table is maintained.
Typevalue shapeUse case
booleantrue / falseFeature flags — SSO, priority support, custom branding
limitinteger or decimalNumeric caps — max seats, max projects, monthly API calls. Set metric_key for automatic usage comparison.
customany JSON objectStructured 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.

GET /v1/entitlements?customer_id= Resolve all entitlements for a customer

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).

200 Response
{
  "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 }
    }
  ]
}
GET /v1/entitlements/check Check a single feature (hot path)

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 vs exceeded: 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).
Caching: For paths above ~100 req/s per customer, cache the resolved set (e.g. 60 s in Redis) and invalidate on subscription change. /check queries usage_aggregates on every call — fast but not free.
200 Limit check
{
  "customer_id":   "cust_acme",
  "feature_key":   "max_api_calls",
  "granted":       true,
  "type":          "limit",
  "value":         1000000,
  "current_usage": 423817,
  "remaining":     576183,
  "exceeded":      false
}
200 Boolean — not granted
{
  "customer_id": "cust_acme",
  "feature_key": "sso",
  "granted":     false,
  "type":        "boolean",
  "value":       false
}
GET /v1/price-plans/:id/versions/:version/entitlements List entitlement definitions for a plan version

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.

Plan Version Management

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.

FieldPurpose
effective_fromSchedule a future version to become the default for new subscriptions automatically. null = available but not yet promoted.
changelogFree-text description of what changed — shown in the UI version history view.
deprecated_atPrevents new subscriptions from referencing this version. Existing subscriptions are honoured until amended.
POST /v1/price-plans/:id/deprecate Deprecate a plan version
Field
Type
Description
versionrequired
string
The version string to deprecate (e.g. "3").
forceoptional
boolean
Allow deprecating a version with active subscriptions. Does not cancel those subscriptions — only sets deprecated_at to block new ones.
POST /v1/price-plans/:id/deprecate
{
  "version": "3",
  "force":   false
}
200 Response
{
  "id":            "plan_growth",
  "version":       "3",
  "deprecated_at": "2026-04-03T10:00:00Z"
}
Payment Processing

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.

Required: Always supply 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.
Charge mechanism
Stripe PaymentIntents API
Payment method token
pm_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.

Charge mechanism
Adyen Checkout /payments (stored method)
Payment method token
recurringDetailReference
Live environment: listRecurringDetails 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.

Full Adyen tokenisation flow
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.