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": "..."
}