Skip to main content
Core.Today

End-Customer Budgets

Set per-customer budgets in reseller mode and track usage with the X-Customer-Id header.

Overview

Reseller mode lets you serve the AI API to many end-customers from a single workspace and API key. Each customer gets an independent prepaid budget (cap), and credits are deducted 1:1. Customer refers to your internal users or service accounts — it never uses real personal data such as email addresses.

One workspace

Handle all customer requests with a single API key

Per-customer budgets

Set an independent credit cap for each customer

Usage delegation

Query per-customer usage history directly via the API

Key terms

  • cap — The cumulative credit limit a customer can use. It only increases via topup.
  • meter — A per-customer cumulative usage counter (spent). It is an advisory value used for limit enforcement; the source of truth for settlement and audit is the usage log.
  • Workspace pool — The workspace's shared balance from which real credits (money) are deducted. The customer meter never moves money.

Where X-Customer-Id applies

A single X-Customer-Id header plays a different role on each of three surfaces. A reseller backend can simply forward this header as-is when proxying an end-customer's request.

Predictions

Billing attribution — Prediction usage is charged to that customer's meter and the budget (cap) is enforced. (See the workflow summary below.)

Files

Per-customer file access + storage isolation — Uploads and re-signing are limited to that customer's files. File Upload docs

Databases

Per-customer document isolation — Document reads/writes/search are filtered to that customer. For reseller teams X-Customer-Id is required (fail-closed; a missing header returns 400 customer_scope_required), and full-dataset access requires an explicit X-Customer-Scope: all header. Databases docs

Quick Start

If reseller mode is enabled (Step 1), the four requests below take you all the way from creating your first customer to billing.

# 1) Check status — confirm reseller_mode: true
curl https://api.core.today/v1/reseller/status \
  -H "X-API-Key: cdt_your_api_key"

# 2) Create a customer (prepaid budget of 1,000 credits)
curl -X POST https://api.core.today/v1/reseller/customers \
  -H "Content-Type: application/json" -H "X-API-Key: cdt_your_api_key" \
  -d { "customer_id": "user_abc123", "cap": 1000 }

# 3) AI request against the customer budget — just add the X-Customer-Id header
curl -X POST https://api.core.today/v1/predictions \
  -H "Content-Type: application/json" -H "X-API-Key: cdt_your_api_key" \
  -H "X-Customer-Id: user_abc123" \
  -d { "model": "black-forest-labs/flux-schnell", "input": { "prompt": "hello" } }

# 4) End-of-month settlement — download the per-customer billing CSV
curl "https://api.core.today/v1/reseller/usage/summary?start_date=2026-06-01T00:00:00Z&end_date=2026-07-01T00:00:00Z&format=csv" \
  -H "X-API-Key: cdt_your_api_key" -o billing.csv
1

Enable reseller mode

Reseller mode is enabled per workspace. On the console's Customers page, apply directly with the "Request reseller mode" button; it activates after the operations team approves. To apply via API, call POST /teams/{teamId}/reseller-request (Clerk JWT auth, owner/admin only), and check progress with GET /teams/{teamId}/reseller-request. If you need help, contact support@core.today.

Note: In a workspace where reseller mode is disabled, sending the X-Customer-Id header is ignored. Budget features only work after activation, and once activated this header becomes required (see Step 4).

2

Check workspace status

Before calling the management endpoints, use GET /reseller/status to check the workspace's reseller status and credit balance. This endpoint can be called with any valid API key regardless of whether reseller mode is on.

curl https://api.core.today/v1/reseller/status \
  -H "X-API-Key: cdt_your_api_key"

# Response — the field layout is identical even when reseller_mode is false
{
  "reseller_mode": true,
  "customer_count": 3,
  "max_customers": 10000,
  "workspace_credits": 5000.0,
  "committed_cap": 12000.0,
  "overcommit_ratio": 2.4,
  "overcommitted": true
}

Tip: When your backend server starts, verify reseller_mode: true to pre-check that the management API is available. workspace_credits approaches 0, you must recharge the workspace so that all customer requests keep working.

Overcommit operations note: committed_cap is the sum of active customer caps. It is allowed for the sum of all customer caps to exceed the workspace pool (overcommit), but in that case overcommitted: true is set (overcommit_ratio = committed ÷ pool, null if the pool is 0) — even if a customer still has budget left, once the workspace pool is exhausted first, all customer requests are blocked with reseller_account_insufficient. Recharge the workspace pool or reduce customer caps. workspace.pool_low webhook (see below) is the early warning for this situation.

3

Create a customer budget

Create a customer wallet by specifying a customer ID and an initial credit cap. The team ID is derived automatically from the API key, so it is not included in the path.

customer_id rules

  • Allowed characters: [A-Za-z0-9_.-]
  • Up to 128 characters
  • Use an opaque ID — no personal data such as email addresses or phone numbers
  • Examples: user_abc123, acct.98765
curl -X POST https://api.core.today/v1/reseller/customers \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -d '{
    "customer_id": "user_abc123",
    "cap": 2000,
    "name": "Acme Corp",
    "external_ref": "optional-your-internal-ref"
  }'

# Response (201 Created)
{
  "customer_id": "user_abc123",
  "cap": 2000,
  "spent": 0,
  "remaining": 2000,
  "status": "active",
  "name": "Acme Corp",
  "external_ref": "optional-your-internal-ref",
  "monthly_allowance": null,
  "next_reset_at": null,
  "reset_due": false,
  "created_at": "2026-07-01T00:00:00+00:00",
  "updated_at": "2026-07-01T00:00:00+00:00"
}
ParameterTypeRequiredDescription
customer_idstringYesCustomer identifier (opaque, no PII)
capnumberYesInitial credit limit (0 or greater, max 1,000,000)
external_refstringNoInternal reference string (arbitrary use, max 512 chars)
namestringNoHuman-readable display name (max 200 chars). Shown on the console customers page.
monthly_allowancenumberNoMonthly budget (greater than 0, max 1,000,000) — see Step 8 "Monthly Budgets". When set, the response includes monthly_allowance, next_reset_at.

Update display metadata (PATCH)

Update display metadata with PATCH /reseller/customers/{id} — the editable fields are name / external_ref only. capcannot be changed via this endpoint — to increase it use the idempotent topup, and use PUT .../budget for monthly budgets.

curl -X PATCH https://api.core.today/v1/reseller/customers/user_abc123 \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -d '{ "name": "Acme Corp (renamed)", "external_ref": "crm-4412" }'

# Response — full wallet (remaining = cap - spent)
{
  "customer_id": "user_abc123",
  "cap": 2000,
  "spent": 350,
  "remaining": 1650,
  "status": "active",
  "name": "Acme Corp (renamed)",
  "external_ref": "crm-4412",
  "monthly_allowance": null,
  "next_reset_at": null,
  "reset_due": false
}

Partial-update rules: Omitted fields stay unchanged. An explicit null or empty string ""removes the field. Sending neither field returns 400.

Bulk onboarding (Bulk Create)

When you have many customers, POST /reseller/customers/bulk can create up to 100 items at once. Partial success is used: each item is processed independently, and successes (created) and failures (errors) are returned separately.

docs.endCustomerBudgets.step3.bulkCode

Handling partial success: One failing item does not roll back the whole request. Always check the errors array in the response and retry only the failed items. However, if customer_limit_exceeded (more than the max of 10,000 customers per team) occurs, the remaining items are not attempted and are all reported in errors. Exceeding the per-request limit of 100 items rejects the entire request (422).

4

Send requests against a customer budget

Adding the X-Customer-Id header to an AI request attributes that request's credits to the customer budget. Per-customer budgets fully apply to both the prediction API (image, video, audio, etc.) and the LLM/chat gateway path.

Header required (fail-closed): On a workspace with reseller mode enabled, every prediction/LLM request requires the X-Customer-Id header. If the header is missing, empty, or malformed, it is rejected with 400 invalid_customer_id. Attribute the reseller workspace's own consumption traffic too by creating a single internal customer_id.

# Image generation request (charged to the customer budget)
curl -X POST https://api.core.today/v1/predictions \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -H "X-Customer-Id: user_abc123" \
  -d {
    "model": "black-forest-labs/flux-schnell",
    "input": {
      "prompt": "A futuristic city skyline"
    }
  }

# LLM/chat request (via the gateway) — the customer budget applies the same way
curl -X POST https://api.core.today/llm/openai/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -H "X-Customer-Id: user_abc123" \
  -d {
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "Hello!"}]
  }

How it works (deduction order): ① First, the customer meter checks and deducts the budget within cap (no money moves — this quickly rejects over-limit customers). ② Then real credits are deducted from the workspace pool. If the customer budget is insufficient, 402 customer_wallet_insufficient is returned; for an unregistered customer ID, 402 customer_not_found is returned — register it first with POST /reseller/customers. If a request fails or is canceled and refunded, both the workspace pool and the customer meter are restored.

5

Top up a customer budget (Top-up)

When a customer's budget runs out, top it up. The request is idempotent, so it is safe to retry on network errors. idempotency_key (1–128 chars, [A-Za-z0-9_.:-]) requests with the same key are processed only once. The response's applied field indicates true (credits added) or false (a duplicate/idempotent no-op).

curl -X POST https://api.core.today/v1/reseller/customers/user_abc123/topup \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -d '{
    "amount": 1000,
    "idempotency_key": "topup-2026-06-30-001"
  }'

# Response
{
  "applied": true,
  "cap": 3000
}

Idempotency: On a duplicate request with the same idempotency_key, it returns applied: false and credits are not added twice. We recommend a unique key based on UUID v4 or a timestamp.

Bulk top-up (Bulk Top-up)

POST /reseller/customers/topup/bulk can top up up to 100 items at once. Each item is idempotently processed by its own idempotency_key, using the partial-success model.

docs.endCustomerBudgets.step5.bulkCode
6

Balance, list, delete

You can query per-customer balances directly, so there is no need to build a separate balance store. Deleting a customer soft-deletes (deactivates) the wallet.

# Get balance
curl https://api.core.today/v1/reseller/customers/user_abc123 \
  -H "X-API-Key: cdt_your_api_key"

# Response — includes the 3 monthly-budget fields (monthly_allowance, next_reset_at, reset_due)
{
  "customer_id": "user_abc123",
  "cap": 3000,
  "spent": 450,
  "remaining": 2550,
  "status": "active",
  "external_ref": null,
  "monthly_allowance": null,
  "next_reset_at": null,
  "reset_due": false
}

# List all customers (cursor pagination, limit 1..500, default 100)
curl "https://api.core.today/v1/reseller/customers?limit=100" \
  -H "X-API-Key: cdt_your_api_key"

# Response — next_cursor is null on the last page
{
  "items": [
    {
      "customer_id": "user_abc123",
      "cap": 3000,
      "spent": 450,
      "status": "active",
      "external_ref": null,
      "monthly_allowance": null,
      "next_reset_at": null,
      "reset_due": false,
      ...
    }
  ],
  "next_cursor": "eyJQSyI6...opaque..."
}

# Next page — pass the received next_cursor back as cursor as-is
curl "https://api.core.today/v1/reseller/customers?cursor=eyJQSyI6...opaque...&limit=100" \
  -H "X-API-Key: cdt_your_api_key"

# Delete a customer (soft delete → 204 No Content)
curl -X DELETE https://api.core.today/v1/reseller/customers/user_abc123 \
  -H "X-API-Key: cdt_your_api_key"

Pagination note: next_cursor is an opaque token — do not parse or construct it yourself; just return it as-is. An invalid cursor is rejected with 400 "Invalid cursor". The list also includes soft-deleted wallets (status: "deleted"), so filter on the client if needed. A single lookup (GET /customers/{id}) returns 404 for a deleted customer.

7

Suspend / reactivate a customer

Instead of deleting a delinquent customer, you can suspend them to block spending immediately while preserving usage history. A suspended customer's requests return the 402 customer_suspended error with an actionable message. Once payment is confirmed, reactivate to restore them immediately.

# Suspend a customer
curl -X POST https://api.core.today/v1/reseller/customers/user_abc123/suspend \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "customer_id": "user_abc123",
  "status": "suspended"
}

# Reactivate a customer
curl -X POST https://api.core.today/v1/reseller/customers/user_abc123/reactivate \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "customer_id": "user_abc123",
  "status": "active"
}

Note: For a customer that does not exist or was deleted, both endpoints return 404. A soft-deleted customer cannot be reactivated (404) — create a new customer.

8

Monthly Budgets

For subscription-style reselling, you can set a monthly budget(monthly_allowance, greater than 0 up to 1,000,000) for a customer. spent keeps accumulating (settlement/reconcile semantics unchanged), and after the 1st-of-month 00:00 UTC boundary passes, on the first deduction that exceeds the remaining budget, lazily the cap is raised to max(cap, spent + allowance) — so each month a customer's remaining budget is guaranteed to be at least the allowance.

Behavior rules

  • Unused topup remainder (the balance above the allowance) is preserved. However, the allowance itself does not carry over or accumulate — even after several unused months it applies only once .
  • Setting a budget does not grant credits immediately. If you need an immediate grant, use topup.
  • reset_due: true indicates that the month boundary has passed and it will be applied automatically on the next deduction that would exhaust the customer's remaining budget. Deductions within the remaining budget proceed without a reset (the cap only increases, so the result is the same).
  • The wallet get/list response includes the three fields monthly_allowance, next_reset_at, reset_due .
docs.endCustomerBudgets.step8.code

Worked example: For a customer with cap 1,500 / spent 1,400 (remaining 100) in a reset_due: true state, when a deduction exceeding the remaining 100 arrives, the cap is raised to max(1500, 1400 + 500) = 1900, restoring the remaining budget to 500 before the deduction. spent is not reset, so settlement/reconcile aggregates stay intact.

Note: A PUT /reseller/customers/{id}/budget for a non-existent customer returns 404. Because a monthly reset raises the cap, committed_cap automatically increases over time — if you have many monthly-budget customers, monitor the workspace pool balance and overcommit status (Step 2) together.

9

Usage records, summary, billing CSV

You can query per-customer raw usage records and period summaries directly, so there is no need to build a separate usage store. Specify the period with start_date / end_date (ISO 8601).

# Raw usage records (limit 1..1000, default 100, newest first)
curl "https://api.core.today/v1/reseller/customers/user_abc123/usage?limit=20" \
  -H "X-API-Key: cdt_your_api_key"

# Response — raw records from the usage log (key fields)
{
  "records": [
    {
      "timestamp": "2026-06-30T11:00:00Z",
      "provider": "black-forest-labs",
      "model": "flux-schnell",
      "credits_used": 10,
      "final_status": "completed",
      "job_id": "job_xxx",
      ...
    }
  ]
}

# Period summary for a specific customer
curl "https://api.core.today/v1/reseller/customers/user_abc123/usage/summary?start_date=2026-06-01T00:00:00Z&end_date=2026-07-01T00:00:00Z" \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "customer_id": "user_abc123",
  "total_credits": 42.5,
  "total_requests": 3,
  "failed_requests": 1,
  "by_model": [
    { "model": "flux-schnell", "credits": 42.5, "count": 3 }
  ]
}

# Workspace-wide customer rollup
curl "https://api.core.today/v1/reseller/usage/summary?start_date=2026-06-01T00:00:00Z&end_date=2026-07-01T00:00:00Z" \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "customers": [
    { "customer_id": "user_abc123", "credits": 42.5, "count": 3, "failed": 1 }
  ],
  "total_credits": 42.5,
  "total_requests": 3,
  "failed_requests": 1
}

# Billing CSV download (header: customer_id,credits,requests,failed_requests)
curl "https://api.core.today/v1/reseller/usage/summary?format=csv" \
  -H "X-API-Key: cdt_your_api_key" \
  -o billing.csv

# Daily breakdown (group_by=day) — supported on both summary endpoints (JSON only)
curl "https://api.core.today/v1/reseller/usage/summary?start_date=2026-06-01T00:00:00Z&end_date=2026-07-01T00:00:00Z&group_by=day" \
  -H "X-API-Key: cdt_your_api_key"

# A by_day array is added to the Response
{
  "customers": [...],
  "total_credits": 42.5,
  "total_requests": 3,
  "failed_requests": 1,
  "by_day": [
    { "date": "2026-06-01", "credits": 12.5, "count": 1, "failed": 0 },
    { "date": "2026-06-02", "credits": 30.0, "count": 2, "failed": 1 }
  ]
}

Billing basis (invoice-grade): total_requests and count count only successful requests . Failed/canceled requests have 0 credits and are reported separately as failed_requests / failed. An errored request is never billed to a customer. group_by=day's by_day array can be used directly as statement line items.

10

Reconcile

You can self-serve verify that the per-customer budget meter matches the usage log. The meter is an advisory value (for reference, rebuildable) for limit enforcement, and the source of truth for settlement and audit is the usage log. The tolerance is 0.01 credits, and within tolerance it returns in_sync: true.

# Single-customer check
curl https://api.core.today/v1/reseller/customers/user_abc123/reconcile \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "customer_id": "user_abc123",
  "meter_spent": 450.0,
  "usage_credits": 450.0,
  "drift": 0.0,
  "in_sync": true
}

# Workspace-wide check (limit 1..1000, default 500)
curl "https://api.core.today/v1/reseller/reconcile?limit=1000" \
  -H "X-API-Key: cdt_your_api_key"

# Response — only customers with drift are returned
{
  "checked": 42,
  "in_sync": 41,
  "drifted": [
    {
      "customer_id": "user_xyz",
      "meter_spent": 120.0,
      "usage_credits": 100.0,
      "drift": 20.0
    }
  ],
  "truncated": false
}

Ops tip: Before monthly settlement, run a workspace reconcile once to confirm that drifted is empty. truncated: true means there are unchecked customers remaining, so raise limit and call again. If drift is found, contact support@core.today — we will rebuild the meter from the usage log.

Caution for LLM traffic before 2026-07-02: LLM/chat usage before 2026-07-02 is reflected in the meter but not in the usage log. Customers with LLM traffic during that period may legitimately show positive (+) drift.

Per-customer file listing

List the files a specific customer created/uploaded (prediction outputs + uploaded inputs) with fresh download URLs. team_id is enforced from the API key and customer_id from the path, so files from other customers cannot mix in. From the console, you can query the same via GET /teams/{team_id}/customers/{customer_id}/files (Clerk JWT).

GET /reseller/customers/{customer_id}/files
curl "https://api.core.today/v1/reseller/customers/user_abc123/files?limit=20" \
  -H "X-API-Key: cdt_your_api_key"

# Response
{
  "files": [
    {
      "object_key": "aiapi/team123/outputs/1734175200000_result.png",
      "filename": "result.png",
      "folder": "outputs",
      "content_type": "image/png",
      "file_size": 1048576,
      "uploaded_at": "2026-07-10T11:00:00Z",
      "job_id": "job_xxx",
      "source": "ai_output",
      "url": "https://files.core.today/aiapi/...&Expires=...",
      "expires_at": "2026-07-17T11:00:00Z"
    }
  ],
  "next_cursor": null
}

folder, content_type, source, start_date/end_date for filtering, and paginate with cursor. Each file's download URL can be adjusted with url_expiration and is capped at 7 days — if you need longer re-access, use POST /files/sign from the File Upload docs.

Phantom tokens — end-customer direct file access

Phantom tokens are short-lived, scoped tokens that let an end-customer read and re-sign their own files directly from a browser or app, without going through the reseller server. The reseller server mints one and hands it to the customer, who then calls the gateway directly with the X-Phantom-Token header. The reseller's real API key is never exposed to the browser.

Flow

  1. 1. The reseller server calls POST /reseller/customers/{id}/token to mint a token (X-API-Key auth).
  2. 2. Hand the minted token to the end-customer (browser/app).
  3. 3. The customer sends the X-Phantom-Token header to hit GET /reseller/customers/{id}/files · POST /files/sign directly.

Token properties

  • TTL — 15 minutes by default (900s), 5 minutes minimum (300s), 60 minutes max (3600s); out-of-range ttl_seconds returns 400. Expiry is embedded in the token.
  • Scope — files:list / files:sign only, files-only (no prediction creation, so no budget spend).
  • Browser CORS — the two consume endpoints allow origin * (non-credential), so a browser can call them directly.
  • Soft revocation — minted tokens are rejected immediately once the wallet is suspended or deleted.
  • Explicit revocation — kill one specific leaked token immediately, before its TTL expires, via POST /reseller/customers/{id}/token/revoke (no need to suspend the whole customer).
  • Revoke all — if tokens leaked but you don't have the plaintext, call POST /reseller/customers/{id}/token/revoke-all to invalidate every unexpired token for the customer at once. The wallet's token_epoch is bumped atomically so everything minted earlier is rejected on verification — no token plaintext needed, and unlike suspend the service keeps running: just mint fresh tokens.
  • Revocation failures are explicit errors — a legacy token minted before revocation existed returns 409 token_not_revocable (suspend the wallet instead); if the revocation store is briefly unavailable you get 503 revocation_unavailable — the token is still live, so retry.
  • Security — the reseller's real API key is never handed to the customer.

Leak response ladder

  1. 1. You have the leaked token string — revoke just that token via POST .../token/revoke (surgical; other tokens keep working).
  2. 2. Tokens leaked but no plaintext — invalidate everything for the customer via POST .../token/revoke-all (service continues; mint fresh tokens right away).
  3. 3. The customer account itself is compromised — suspend the wallet via POST .../suspend (all tokens are auto-invalidated AND all spending stops until you reactivate).
# 1) Reseller server mints a token (X-API-Key)
curl -X POST https://api.core.today/v1/reseller/customers/user_abc123/token \
  -H "Content-Type: application/json" \
  -H "X-API-Key: cdt_your_api_key" \
  -d '{ "ttl_seconds": 900, "scope": ["files:list", "files:sign"] }'

# Response
{
  "token": "eyJ2IjoxLC4uLn0.c2ln",
  "expires_at": "2026-07-13T00:15:00+00:00",
  "scope": ["files:list", "files:sign"],
  "customer_id": "user_abc123"
}

# 2) End-customer (browser/app) calls the gateway directly — no API key
curl https://api.core.today/v1/reseller/customers/user_abc123/files \
  -H "X-Phantom-Token: eyJ2IjoxLC4uLn0.c2ln"

curl -X POST https://api.core.today/v1/files/sign \
  -H "Content-Type: application/json" \
  -H "X-Phantom-Token: eyJ2IjoxLC4uLn0.c2ln" \
  -d '{ "object_key": "aiapi/team123/outputs/1734175200000_result.png" }'

# 3) Leak response without the token plaintext: kill every outstanding token
curl -X POST https://api.core.today/v1/reseller/customers/user_abc123/token/revoke-all \
  -H "X-API-Key: cdt_your_api_key"
# → { "revoked": true, "customer_id": "user_abc123", "token_epoch": 3 }

# --- Node SDK: mint from the reseller server ---
const { token, expires_at } = await client.reseller.mintCustomerToken(
  "user_abc123",
  { ttlSeconds: 900, scope: ["files:list", "files:sign"] },
);

Webhook alerts

Subscribe to the events below in your existing webhook config (console /webhooks or API) to monitor customer budgets and workspace pool status without polling.

EventTrigger conditionPayload
customer.balance_lowA customer's remaining budget falls to 10% or less of the capteam_id, customer_id, remaining, cap, spent, threshold
customer.cap_reachedA charge is rejected because the budget is exceededteam_id, customer_id, cap, spent
workspace.pool_lowThe workspace shared pool falls below the threshold — default 100 credits, per-team override: settings.pool_low_thresholdteam_id, remaining_credits, threshold

Deduplication (dedup): The same state is not sent repeatedly (6-hour window). When the balance recovers via top-up or a monthly budget reset, the alert re-arms.

Rate limits

The heavy aggregation/file endpoints below have a dedicated, much stricter budget on top of your API key's general limits:

  • GET /reseller/customers/{id}/files
  • GET /reseller/customers/{id}/usage/summary
  • GET /reseller/usage/summary
  • GET /reseller/customers/{id}/reconcile
  • GET /reseller/reconcile

The dedicated budget is 10/min · 200/hour · 2,000/day. Exceeding it returns 429with a Retry-Afterheader (seconds) — wait that long before retrying.

Bulk settlement tip: Don't loop per-customer summaries — one call to the workspace-wide rollup GET /reseller/usage/summary returns billing data for every customer at once.

Error codes

Alongside the human-readable detail , billing-path (prediction/LLM) error responses include a machine-readable error.code field (402 errors also include error.required / error.available / error.customer_id). Branch your retry/top-up logic on error.code, not the detail string.

HTTPCodeDescription & action
400invalid_customer_idX-Customer-Id missing or malformed (billing path; the header is required in reseller mode). On management endpoints, a malformed customer_id or an invalid cursor is returned as 400 with a detail message.
402customer_not_foundCustomer not registered (billing path) → create it first with POST /reseller/customers.
402customer_wallet_insufficientCustomer budget exhausted (billing path) → recharge via top-up.
402customer_suspendedCustomer is suspended (billing path) → reactivate with POST /reseller/customers/{id}/reactivate.
402reseller_account_insufficientWorkspace pool exhausted (billing path) → recharge workspace credits with a recharge code
403(detail message)"Reseller mode is not enabled for this workspace" — Management endpoints (/reseller/customers* etc.) are available only on workspaces with reseller mode enabled ( GET /reseller/status is the exception)
404(detail message)"Customer not found" — The target customer for get/top-up/budget/suspend/reactivate does not exist or was soft-deleted
409(detail message)"Customer wallet already exists" — A wallet with the same customer_id already exists (in bulk, reported per item as wallet_exists error)
429(detail message)"Customer limit exceeded for this team" — Exceeded the maximum number of customers per team (10,000)

Using the SDKs

The official Node.js/Python SDKs include a reseller namespace, so you can call all of the endpoints above with types. For prediction calls, the customerId / customer_id option automatically sets the X-Customer-Id header (as a client default or per request).

// Node.js — npm install @coredot/aiapi
import { AIAPI } from "@coredot/aiapi";

const client = new AIAPI({ apiKey: "cdt_your_api_key" });
await client.reseller.createCustomer({ customer_id: "user_abc123", cap: 1000 });
await client.predictions.create({
  model: "black-forest-labs/flux-schnell",
  input: { prompt: "hello" },
  customerId: "user_abc123",
});

# Python — pip install coredot-aiapi
from aiapi import AIAPI

client = AIAPI(api_key="cdt_your_api_key")
client.reseller.create_customer(customer_id="user_abc123", cap=1000)
client.predictions.create(
    model="black-forest-labs/flux-schnell",
    input={"prompt": "hello"},
    customer_id="user_abc123",
)

Workflow summary

  1. 1. Apply for "Request reseller mode" on the console's Customers page → operations team approval
  2. 2. GET /reseller/status reseller_mode: true check and review the workspace credit balance
  3. 3. POST /reseller/customers — register a customer and set the initial cap (use /bulk for bulk)
  4. 4. Include the X-Customer-Id header on every AI request (prediction + LLM) (required in reseller mode)
  5. 5. When a customer budget runs out, top up with POST /reseller/customers/{id}/topup, and for subscriptions set a monthly budget (monthly_allowance)
  6. 6. Block a delinquent customer immediately with POST /reseller/customers/{id}/suspend, then after payment is confirmed reactivate to reactivate
  7. 7. At settlement time, download the billing CSV with GET /reseller/usage/summary?format=csv
  8. 8. Before monthly settlement, confirm the meter matches the usage log with GET /reseller/reconcile

Manage from the workspace console

Beyond the API, customer budgets can be managed directly from the workspace console. Workspace → Customers page lets you create customers, list them, top up, set monthly budgets, suspend/reactivate, review usage, and delete — all from the UI. The console uses workspace login (Clerk), while the /reseller/* API uses an API key ( X-API-Key).