driptab/Docs

API reference

All endpoints are under /v1. Authentication is via bearer token:

Authorization: Bearer dtab_…

Most endpoints require a project header:

X-Project-Id: <project_uuid>

Errors

All errors use the shape:

{
  "error": {
    "code": "not_found",
    "message": "Plan not found",
    "retryable": false
  }
}
Code Status Meaning
unauthorized 401 Missing or invalid token
forbidden 403 Token lacks required scope
not_found 404 Resource not found
missing_project 400 X-Project-Id header required but absent
has_active_subscriptions 422 Cannot archive plan with active subscriptions
has_dependencies 422 Cannot archive catalog item with active references
validation_error 422 Request body failed schema validation

Tokens

Tokens are unauthenticated — provide organization_id in the body.

POST /v1/keys

Create a new API token.

{
  "name": "MCP Agent",
  "organization_id": "org-uuid",
  "scopes": ["read"]
}

Response 201:

{
  "token": "dtab_abc123…",
  "id": "uuid",
  "key_prefix": "dtab_abc123",
  "name": "MCP Agent"
}

The token value is shown once and never stored.

GET /v1/keys

List tokens for the authenticated organization.

Response 200:

{
  "tokens": [
    {
      "id": "uuid",
      "name": "MCP Agent",
      "key_prefix": "dtab_abc123",
      "scopes": ["read"],
      "last_used_at": "2026-06-08T14:00:00.000Z",
      "created_at": "2026-06-01T00:00:00.000Z"
    }
  ]
}

GET /v1/keys/me

Return the token and organization for the currently authenticated request.

DELETE /v1/keys/:id

Revoke a token. Response 204.


Customers

POST /v1/customers

{
  "external_id": "acme-user-1234",
  "name": "Jane Smith",
  "email": "jane@example.com",
  "currency": "USD"
}

Response 201:

{
  "customer": {
    "id": "uuid",
    "short_id": "cust_ab12",
    "external_id": "acme-user-1234",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "currency": "USD",
    "created_at": "2026-06-08T00:00:00.000Z",
    "updated_at": "2026-06-08T00:00:00.000Z"
  }
}

GET /v1/customers

Response 200: { "customers": [...] }

With external_id, returns a single customer:

GET /v1/customers?external_id=acme-user-1234

Response 200: { "customer": {...} }

GET /v1/customers/:id

Accepts UUID or ext:<external_id>. Response 200: { "customer": {...} }

PATCH /v1/customers/:id

Update customer fields. All fields optional.

Field Type Required Description
name string No Customer name
email string No Email address
currency string No 3-letter ISO-4217 currency code

Response 200: { "customer": {...} }. Also invalidates portal cache. Returns 404 if not found.


Billable metrics

POST /v1/metrics

{
  "code": "mail_out",
  "name": "Outbound Emails",
  "aggregation_type": "sum",
  "field_name": "count"
}

Response 201: { "metric": {...} }

GET /v1/metrics

Response 200: { "metrics": [...] }

With code, returns a single metric:

GET /v1/metrics?code=api_calls

Response 200: { "metric": {...} }

GET /v1/metrics/:id


Catalog

POST /v1/catalog

Upsert-by-code semantics. Creates or updates the catalog item with this code.

{
  "code": "vps.cx11",
  "name": "VPS CX11 — 2 vCPU / 4 GB / 40 GB SSD",
  "amount_cents": 415,
  "currency": "USD",
  "metadata": { "vcpu": 2, "ram_gb": 4 }
}

Response 201: { "catalog_item": {...} }

GET /v1/catalog

Response 200: { "catalog_items": [...] }

GET /v1/catalog/:id

DELETE /v1/catalog/:id

Archives the catalog item. Returns 422 with code: has_dependencies if referenced by active charges or add-ons.


Plans

POST /v1/plans

Accepts an optional labels field (default []):

{
  "code": "vps-cx11-monthly",
  "name": "VPS CX11 Monthly",
  "interval": "monthly",
  "amount_cents": 0,
  "amount_currency": "USD",
  "labels": ["domain", "reseller"],
  "charges": [
    {
      "charge_model": "flat_fee",
      "catalog_item_code": "vps.cx11",
      "invoice_timing": "arrears"
    }
  ]
}

For a standard (usage-based) charge:

{
  "charge_model": "standard",
  "billable_metric_id": "uuid-of-metric",
  "invoice_timing": "arrears",
  "properties": { "amount": "0.0015" }
}

Response 201:

{
  "plan": {
    "id": "uuid",
    "code": "vps-cx11-monthly",
    "name": "VPS CX11 Monthly",
    "interval": "monthly",
    "amount_cents": 0,
    "amount_currency": "USD",
    "archived_at": null,
    "created_at": "...",
    "charges": [...]
  }
}

GET /v1/plans

Response 200: { "plans": [...] }

With code, returns a single plan:

GET /v1/plans?code=domain-register-com

Response 200: { "plan": {...} }

GET /v1/plans/:id

Returns plan with its charges.

POST /v1/plans/:id/charges

Add a charge to an existing plan. Same body as the charge objects in POST /v1/plans. Response 201: { "charge": {...} }

PATCH /v1/plans/:id

Update plan metadata. At least one field required.

Field Type Required Description
labels string[] No Replace labels array e.g. ["domain","reseller"]
category string|null No Plan category
name string No Display name
description string|null No Description

Response 200: { "plan": {...} }. Returns 404 if not found.

DELETE /v1/plans/:id

Archives the plan. Returns 422 with code: has_active_subscriptions if the plan has active subscriptions. Response 204 on success.


Add-ons

GET /v1/addons

Response 200: { "add_ons": [...] }

With code, returns a single add-on:

GET /v1/addons?code=domain-transfer-com

Response 200: { "add_on": {...} }

POST /v1/addons/:id/apply

Apply an add-on to a customer. Generates an immediate invoice.

{
  "customer_id": "uuid-of-customer"
}

Response 201:

{
  "applied_add_on": {
    "id": "uuid",
    "customer_id": "...",
    "add_on_id": "...",
    "invoice_id": "...",
    "amount_cents": 1296,
    "currency": "USD",
    "applied_at": "2026-06-08T00:00:00.000Z"
  }
}

Subscriptions

POST /v1/subscriptions

{
  "external_id": "your-sub-id",
  "customer_id": "uuid-of-customer",
  "plan_id": "uuid-of-plan",
  "properties": { "node_count": 5 }
}

The API validates customer_id and plan_id as UUIDs. The dtab CLI accepts friendly references such as --customer ext:acme-user-1234 and --plan domain-register-com, resolves them through the lookup endpoints, and then sends UUIDs to the API.

Response 201:

{
  "subscription": {
    "id": "uuid",
    "external_id": "your-sub-id",
    "customer_id": "...",
    "plan_id": "...",
    "status": "active",
    "billing_time": "calendar",
    "properties": "{ \"node_count\": 5 }",
    "started_at": "2026-06-08T00:00:00.000Z",
    "current_period_start": "2026-06-08T00:00:00.000Z",
    "current_period_end": "2026-06-30T23:59:59.000Z",
    "terminated_at": null,
    "created_at": "..."
  }
}

GET /v1/subscriptions

Response 200: { "subscriptions": [...] }

GET /v1/subscriptions/:id

Accepts UUID or ext:<external_id>.

POST /v1/subscriptions/:id/terminate

Terminate a subscription. Response 200: { "subscription": { "status": "terminated", ... } }

POST /v1/subscriptions/:id/suspend

Suspend a subscription. Response 200.

PATCH /v1/subscriptions/:id

Update subscription properties or status. At least one field required.

Field Type Required Description
properties object No Merged into existing properties (e.g. {"domain":"example.com"})
status string No active, suspended, terminated, or canceled

Response 200: { "subscription": {...} }


Events

POST /v1/events

{
  "transaction_id": "unique-event-id",
  "subscription_id": "uuid-or-ext:id",
  "code": "mail_out",
  "value": "1",
  "timestamp": "2026-06-08T14:23:00.000Z",
  "properties": {}
}

Response 200:

{ "accepted": true, "duplicate": false }

If transaction_id was already seen: { "accepted": true, "duplicate": true }.


Invoices

GET /v1/invoices

Query parameters: customer_id, status, payment_status, period_start, period_end, limit, cursor.

Response 200:

{
  "invoices": [
    {
      "id": "uuid",
      "number": "INV-2026-0001",
      "status": "outstanding",
      "payment_status": "pending",
      "invoice_type": "subscription",
      "currency": "USD",
      "subtotal_cents": 847,
      "total_cents": 847,
      "issuing_date": "2026-07-01",
      "due_date": "2026-07-15",
      "period_start": "2026-06-01",
      "period_end": "2026-06-30",
      "created_at": "..."
    }
  ],
  "cursor": "..."
}

GET /v1/invoices/:id

Returns invoice with its fees.

{
  "invoice": {
    ...
    "fees": [
      {
        "id": "uuid",
        "fee_type": "charge",
        "billable_metric_code": "mail_out",
        "units": "640",
        "amount_cents": 128,
        "amount_currency": "USD",
        "properties": { "amount": "0.0002" }
      }
    ]
  }
}

Payments

POST /v1/payments

{
  "invoice_id": "uuid",
  "amount_cents": 847,
  "currency": "USD",
  "provider": "stripe",
  "provider_payment_id": "pi_3abc123",
  "paid_at": "2026-07-05T14:22:00.000Z",
  "reference": "optional note"
}

Response 201:

{
  "payment": {
    "id": "uuid",
    "invoice_id": "...",
    "amount_cents": 847,
    "currency": "USD",
    "provider": "stripe",
    "provider_payment_id": "pi_3abc123",
    "status": "succeeded",
    "paid_at": "2026-07-05T14:22:00.000Z",
    "created_at": "..."
  }
}

Webhook endpoints

POST /v1/webhooks

{
  "url": "https://app.example.com/hooks/driptab",
  "events": ["invoice.created", "payment.succeeded"]
}

Response 201:

{
  "webhook_endpoint": {
    "id": "uuid",
    "url": "https://app.example.com/hooks/driptab",
    "events": ["invoice.created", "payment.succeeded"],
    "secret": "whsec_abc123def456…",
    "enabled": 1,
    "created_at": "..."
  }
}

The secret is returned once in full at creation. Subsequent list responses mask it.

GET /v1/webhooks

DELETE /v1/webhooks/:id


Analytics

GET /v1/analytics

Requires X-Project-Id. Returns project billing metrics.

Response 200:

{
  "active_subscriptions": 42,
  "mrr_cents": 129900,
  "outstanding_invoice_count": 3,
  "outstanding_total_cents": 45000,
  "revenue_30d_cents": 890000,
  "payments_30d_count": 28,
  "top_plans": [
    {
      "plan_code": "vps-cx11-monthly",
      "plan_name": "VPS CX11 Monthly",
      "active_count": 18
    }
  ]
}

Pagination & limits

All list endpoints (GET /v1/customers, /v1/subscriptions, /v1/invoices, /v1/payments, /v1/billing-events) enforce:

  • Default limit: 10 records
  • ?limit=N — custom limit (max 1000)
  • ?all=1 — return up to 1000 records
  • ?offset=N — skip N records

The X-Project-Id header is required on all list endpoints. Missing → 400 missing_project.


Portal

GET /v1/portal/:customerId

Public, no authentication required. Returns the customer portal data. Responses are cached in R2 for 30 days.

GET /v1/portal/:customerId?refresh=1

Add ?refresh=1 to bypass the cache and force a fresh fetch.

Cache management

Requires Bearer authentication.

DELETE /v1/portal-cache/:customerId   — purge single customer cache
DELETE /v1/portal-cache               — purge all portal cache entries

Responses:

{ "purged": true, "customer_id": "..." }
{ "purged": true, "count": 42 }

MCP integration

POST /v1/mcp   — JSON-RPC 2.0
GET  /v1/mcp   — server info / discovery

Authentication: same Bearer token as all other endpoints.

Available tools: list_customers, get_customer, list_subscriptions, get_subscription, list_invoices, get_invoice, list_payments, list_plans, get_plan, record_payment.

curl -X POST https://api.driptab.app/v1/mcp \
  -H "Authorization: Bearer dtab_..." \
  -H "X-Project-Id: <project_id>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}'

Billing events

GET /v1/billing-events

Query the domain event log.

Query parameters: resource_type, resource_id, event_type, actor_token_id, since, limit, cursor.

Response 200:

{
  "billing_events": [
    {
      "id": "uuid",
      "resource_type": "customer",
      "resource_id": "uuid",
      "event_type": "customer.created",
      "actor_token_id": "uuid",
      "payload": { "external_id": "acme-user-1234" },
      "created_at": "2026-06-08T00:00:00.000Z"
    }
  ],
  "cursor": "..."
}