Partner API
Use the Partner API when your product team needs to onboard customer organizations programmatically while Quotaflow remains the visible API platform. The API is independent from dashboard user sessions and uses a partner machine token.
Base URL and authentication
export QUOTAFLOW_PARTNER_TOKEN="qfp_your_partner_token"
export QUOTAFLOW_PARTNER_BASE_URL="https://api.quotaflow.ai/partner/v1"
Send the token as a bearer token:
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Create and rotate partner machine tokens in Dashboard → Partner → Automation Tokens. The full qfp_... token is shown once; store it in your product secret manager. Choose the smallest scope your automation needs:
| Scope | Allows |
|---|---|
partner:read | Every GET route: profile, funding, customers, customer balance, model groups and prices, API key previews, and usage. |
partner:write | All read actions plus every PUT, POST and DELETE route: customer creation, allocation changes, per-group price changes, top-up link creation, and customer API key creation and disabling. Write routes also need a partner role of owner, admin or operator; a viewer's token is refused with PARTNER_ACCESS_REQUIRED. |
Keep partner tokens separate from customer routing keys:
| Credential | Prefix | Where to manage | Use for |
|---|---|---|---|
| Partner automation token | qfp_... | Dashboard → Partner → Automation Tokens | /partner/v1/* automation only. |
| Customer routing API key | qf_... | Partner customer organization/API key flows | OpenAI-compatible model calls from that customer. |
All write requests require an Idempotency-Key header. Use a stable key per operation, such as customer-cust_123-create-v1. Reusing a key with the same body replays the first result; reusing it with a different body returns 409 IDEMPOTENCY_KEY_CONFLICT.
Every response uses one envelope. Success is {"code": 0, "message": "success", "data": ...}; an error is {"code": <HTTP status>, "reason": "<CODE>", "message": "...", "metadata": {...}}. Branch on reason, not on message.
Rate limits
Each partner token may make 60 read requests (GET) and 30 write requests per minute, counted in fixed one-minute windows. Over the limit the API answers 429 with reason PARTNER_RATE_LIMITED and a Retry-After header in seconds; wait that long before retrying. Usage figures refresh about once a minute, so polling usage or balance more often than that returns no new data.
Customer lifecycle
A customer goes through these steps, always in this order:
- Create.
PUT /customers/by-external-id/{external_customer_id}creates the customer organization with statuspending_activationand emails an invitation tocontact_email.initial_allocation_usdmay be0: the customer is created unfunded — nothing is reserved from your funding, and it cannot spend until you fund it in step 4. - Invitation. The email invites
contact_emailto become the owner of the new customer organization. Only the person who controls that mailbox can accept it: they sign in (or sign up) with that exact email address and accept. The create response returns the same link once asactivation.invite_url, so you can also hand it over yourself. The invitation lives 14 days from creation (activation.invitation_expires_at). - Activation. When the invitation is accepted the customer becomes
activein the same transaction, and its organization balance opens with the allocation it was created with (0for an unfunded customer). Until then, balance, allocations, model groups, price changes, API keys and usage answer409 PARTNER_CUSTOMER_PENDING_ACTIVATION, whosemetadatacarriesinvited_email,invitation_status,invitation_expires_atandnext_action: accept_invitation. - Fund.
POST /customers/{customer_id}/allocationswithincreaseorset_limitcredits the customer's organization balance from your partner funding. - Keys.
POST /customers/{customer_id}/api-keyscreates the customer'sqf_...key for one of its model groups. - Call a model. The customer (or your product on its behalf) calls
https://api.quotaflow.ai/openai/v1with that key; every call spends from the customer's organization balance.
Customer status values:
status | Meaning | What to do |
|---|---|---|
pending_activation | Created; waiting for contact_email to accept the invitation. | Ask the contact to accept the email, or send them activation.invite_url from the create response. |
active | Invitation accepted; the organization balance is open. | Fund it, create keys, call models. |
activation.invitation_status says where the invitation stands: pending, accepted, expired (14 days passed without acceptance) or revoked. Every customer response also carries an activation_hint sentence saying what happens next.
If the invitation expires the customer stays pending_activation and can no longer activate by itself: an expired invitation cannot be accepted, there is no partner route that re-sends it, and repeating the create PUT returns the existing customer without sending a new email. Contact Quotaflow support to re-issue the invitation. A funded customer's initial allocation stays reserved from your funding while it is pending; an unfunded one reserves nothing.
Endpoint index
All paths are relative to https://api.quotaflow.ai/partner/v1. Every route can also answer 401 PARTNER_API_AUTHENTICATION_REQUIRED / PARTNER_API_TOKEN_INVALID, 403 PARTNER_API_SCOPE_REQUIRED / PARTNER_ACCESS_REQUIRED, and 429 PARTNER_RATE_LIMITED; write routes also answer 400 IDEMPOTENCY_KEY_REQUIRED and 409 IDEMPOTENCY_KEY_CONFLICT / IDEMPOTENCY_IN_PROGRESS. The table lists the codes specific to each route.
| Method | Path | Scope | Purpose | Route-specific 4xx |
|---|---|---|---|---|
GET | /me | read | Your partner identity and token scopes. | — |
GET | /funding | read | Your partner balance: available funding, reserved allocations, outstanding usage, top-up URL. | — |
POST | /funding/top-up-sessions | write | A hosted billing link to add partner funds. | 400 INVALID_PARTNER_TOP_UP_AMOUNT |
PUT | /customers/by-external-id/{external_customer_id} | write | Create a customer, or return the existing one for the same id. | See create errors. |
GET | /customers/by-external-id/{external_customer_id} | read | Look a customer up by your own id. | 400 INVALID_PARTNER_EXTERNAL_CUSTOMER_ID, 404 PARTNER_CUSTOMER_NOT_FOUND |
GET | /customers/{customer_id} | read | Read one customer: status, allocation, activation. | 404 PARTNER_CUSTOMER_NOT_FOUND |
GET | /customers/{customer_id}/balance | read | The customer's organization balance and allocation. | 404 PARTNER_CUSTOMER_NOT_FOUND, 409 PARTNER_CUSTOMER_PENDING_ACTIVATION |
POST | /customers/{customer_id}/allocations | write | Fund the customer (increase, decrease, set_limit). | 400 INVALID_PARTNER_ALLOCATION_ACTION / INVALID_PARTNER_ALLOCATION_AMOUNT / INVALID_PARTNER_ALLOCATION_LIMIT / PARTNER_ALLOCATION_LIMIT_BELOW_USED, 403 PARTNER_FUNDING_INSUFFICIENT, 404 PARTNER_CUSTOMER_NOT_FOUND, 409 PARTNER_CUSTOMER_PENDING_ACTIVATION / PARTNER_CUSTOMER_BALANCE_RESERVED |
GET | /customers/{customer_id}/groups | read | The customer's model groups with its price and your wholesale rate and floor per group. | 404 PARTNER_CUSTOMER_NOT_FOUND, 409 PARTNER_CUSTOMER_PENDING_ACTIVATION |
PUT | /customers/{customer_id}/groups/{group_id}/rate | write | Change the price (retail_rate_multiplier) the customer pays on one group. | 400 INVALID_PARTNER_CUSTOMER_GROUP_RATE / PARTNER_RETAIL_RATE_BELOW_FLOOR, 403 PARTNER_FUNDING_INSUFFICIENT, 404 PARTNER_CUSTOMER_NOT_FOUND / PARTNER_CUSTOMER_GROUP_NOT_FOUND, 409 PARTNER_CUSTOMER_PENDING_ACTIVATION |
GET | /customers/{customer_id}/api-keys | read | The customer's key previews (never the full key). | 404 PARTNER_CUSTOMER_NOT_FOUND |
POST | /customers/{customer_id}/api-keys | write | Create a qf_... key for one model group. | 400 INVALID_PARTNER_CUSTOMER_API_KEY, 404 PARTNER_CUSTOMER_NOT_FOUND (also for a group the customer does not have), 409 PARTNER_CUSTOMER_PENDING_ACTIVATION |
DELETE | /customers/{customer_id}/api-keys/{api_key_id} | write | Disable a key permanently. | 400 INVALID_PARTNER_CUSTOMER_API_KEY, 404 PARTNER_CUSTOMER_API_KEY_NOT_FOUND |
GET | /customers/{customer_id}/usage | read | The whole customer's usage buckets. | 400 INVALID_PARTNER_USAGE_GRANULARITY / INVALID_PARTNER_USAGE_WINDOW, 404 PARTNER_CUSTOMER_NOT_FOUND, 409 PARTNER_CUSTOMER_PENDING_ACTIVATION |
GET | /customers/{customer_id}/api-keys/{api_key_id}/usage | read | One key's usage buckets. | as /usage, plus 404 PARTNER_CUSTOMER_API_KEY_NOT_FOUND |
These paths do not exist and answer a plain 404 page not found (not a JSON envelope); use the route on the right:
| You might try | Use instead |
|---|---|
GET /balance | GET /funding for your partner balance; GET /customers/{customer_id}/balance for one customer's. |
GET /customers | GET /customers/by-external-id/{external_customer_id} or GET /customers/{customer_id} (the full list is in Dashboard → Partner → Customers). |
GET /models, GET /pricing, GET /pricing/models | GET /customers/{customer_id}/groups for the customer's model groups and prices; change a price with PUT /customers/{customer_id}/groups/{group_id}/rate. See Pricing. |
Pricing
All prices are multipliers of the official per-model price published in Models. Three numbers matter, per model group:
| Term | Who sets it | Meaning | Where to read it |
|---|---|---|---|
wholesale_rate_multiplier | Quotaflow, in your partner agreement (discount envelope) | What you pay per unit of official price when your customer uses the group. | GET /customers/{customer_id}/groups → wholesale_rate_multiplier |
retail_rate_multiplier | You | What your customer pays per unit of official price. Set for every group at create; change one group with PUT .../groups/{group_id}/rate. | GET /customers/{customer_id}/groups → rate_multiplier |
Retail floor (min_rate_multiplier, the minimum price) | Quotaflow, in your partner agreement | The lowest retail_rate_multiplier you may set: the larger of the envelope's min_rate_multiplier and its wholesale rate. | GET /customers/{customer_id}/groups → retail_floor_rate_multiplier |
A rate below the floor is refused with 400 PARTNER_RETAIL_RATE_BELOW_FLOOR and changes nothing. At create, one retail_rate_multiplier applies to all your groups, so it must be at or above the highest floor across them.
Your funding is reserved at wholesale, not retail: funding a customer with X USD reserves X × wholesale ÷ retail (the highest ratio across its groups) from your partner funding, shown as reserved_allocations_usd on GET /funding. Lowering a customer's price raises that ratio, so a price change can need more funding and is refused with 403 PARTNER_FUNDING_INSUFFICIENT when you do not have it.
Funding
curl "$QUOTAFLOW_PARTNER_BASE_URL/funding" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Example response:
{
"code": 0,
"message": "success",
"data": {
"reseller_id": 42,
"available_funding_usd": 250.0,
"reserved_allocations_usd": 100.0,
"outstanding_usage_usd": 12.5,
"top_up_url": "https://app.quotaflow.ai/billing"
}
}
If you need more funds, create a top-up action URL:
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/funding/top-up-sessions" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: topup-100-2026-07-02" \
-H "Content-Type: application/json" \
-d '{"amount_usd":100}'
Create or find a customer organization
Use your own stable customer id in the URL. Repeating the same request with the same external_customer_id returns the existing customer (created: false) and sends no new email.
| Field | Required | Rule |
|---|---|---|
external_customer_id (path) | yes | Your id; letters, numbers, ., _, :, -; at most 128 characters. |
name | yes | Customer organization name. |
slug | no | Organization slug; derived from name when omitted. |
contact_email | yes | The person who receives the invitation and becomes the organization owner on acceptance. |
billing_email | no | Billing contact stored on the organization. |
initial_allocation_usd | yes | USD to fund the customer with on activation. 0 is allowed and creates the customer unfunded: nothing is reserved from your funding, and it cannot spend until you fund it with POST /customers/{customer_id}/allocations after activation. Must be >= 0. |
retail_rate_multiplier | yes | The price multiplier the customer pays on every group; no default; at or above your retail floor. |
curl -X PUT "$QUOTAFLOW_PARTNER_BASE_URL/customers/by-external-id/cust_123" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-cust_123-create-v1" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme AI",
"contact_email": "owner@acme.example",
"initial_allocation_usd": 0,
"retail_rate_multiplier": 1.0
}'
Example response:
{
"code": 0,
"message": "success",
"data": {
"external_customer_id": "cust_123",
"created": true,
"status": "pending_activation",
"customer": {
"id": 101,
"organization_name": "Acme AI",
"organization_slug": "partner-42-acme-ai",
"display_name": "Acme AI",
"email": "owner@acme.example",
"billing_mode": "partner_bill",
"billing_allocation_mode": "org_direct",
"partner_allocation_cap_usd": 0,
"partner_allocation_used_usd": 0,
"partner_allocation_remaining_usd": 0,
"organization_balance_usd": 0,
"organization_available_usd": 0,
"status": "pending_activation"
},
"activation": {
"method": "invitation",
"invited_email": "owner@acme.example",
"invitation_status": "pending",
"invitation_expires_at": "2026-10-22T21:29:00Z",
"invite_url": "https://app.quotaflow.ai/invite?token=..."
},
"activation_hint": "Pending activation: the customer becomes active when owner@acme.example accepts the invitation emailed to it before 2026-10-22T21:29:00Z. Balance, allocations, API keys and usage answer 409 PARTNER_CUSTOMER_PENDING_ACTIVATION until then. It has no allocation (initial_allocation_usd 0): after activation it cannot spend until you increase it with POST /partner/v1/customers/101/allocations."
}
}
activation.invite_url appears only on the response that created the customer; an idempotent replay and every later read omit it.
Create errors
| HTTP | reason | Meaning |
|---|---|---|
| 400 | INVALID_PARTNER_EXTERNAL_CUSTOMER_ID | The path id is empty, too long, or has characters outside letters, numbers, ., _, :, -. |
| 400 | INVALID_PARTNER_CUSTOMER | name is missing, or no slug can be derived from it. |
| 400 | INVALID_PARTNER_CUSTOMER_CONTACT | contact_email is not a valid email. |
| 400 | INVALID_PARTNER_INITIAL_ALLOCATION | initial_allocation_usd is negative. |
| 400 | INVALID_PARTNER_RETAIL_RATE | retail_rate_multiplier is missing or not positive. |
| 400 | PARTNER_RETAIL_RATE_BELOW_FLOOR | retail_rate_multiplier is below your retail floor. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing. |
| 403 | PARTNER_FUNDING_INSUFFICIENT | Your available funding does not cover the reservation for initial_allocation_usd; metadata carries available_funding_usd, required_usd, shortfall_usd and next_action: top_up_partner_balance. Never returned for 0. |
| 403 | PARTNER_API_SCOPE_REQUIRED / PARTNER_ACCESS_REQUIRED | The token lacks partner:write, or its user is a viewer. |
| 409 | PARTNER_EXTERNAL_CUSTOMER_CONFLICT | The external id is already taken by another customer of yours. |
| 409 | PARTNER_CUSTOMER_SLUG_EXISTS | The organization slug is taken; send another slug. |
| 409 | IDEMPOTENCY_KEY_CONFLICT | The same Idempotency-Key was used with a different body. |
Nothing is created or reserved when any of these is returned.
Read a customer
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/by-external-id/cust_123" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Both return the create response's data without created: external_customer_id, status, customer, activation and activation_hint. An unknown id is 404 PARTNER_CUSTOMER_NOT_FOUND.
Manage customer funding
Funding needs an active customer. Increase the customer organization balance and its reporting target:
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/allocations" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-allocation-increase-25" \
-H "Content-Type: application/json" \
-d '{"action":"increase","amount_usd":25}'
Set the partner-funded reporting target and adjust the organization balance by the corresponding delta:
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/allocations" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-allocation-set-75" \
-H "Content-Type: application/json" \
-d '{"action":"set_limit","limit_usd":75}'
Read the current balance:
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/balance" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Partner funding is credited, with idempotent audit provenance, to the customer organization's balance before it can be spent. Every project and key in that customer organization then spends from the same organization balance; project allocations remain attribution and reporting records, not wallets.
Create a customer API key
First list the model groups the customer can use, with its price on each:
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/groups" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
{
"code": 0,
"message": "success",
"data": [
{
"id": 123,
"name": "All models",
"description": "",
"product_code": "all_models",
"rate_multiplier": 1.0,
"status": "active",
"wholesale_rate_multiplier": 0.8,
"retail_floor_rate_multiplier": 0.8
}
]
}
Change the price on one group (the body takes retail_rate_multiplier only):
curl -X PUT "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/groups/123/rate" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-group-123-rate-0.9" \
-H "Content-Type: application/json" \
-d '{"retail_rate_multiplier":0.9}'
Then create a customer key. The full qf_... key is returned only on the first successful creation response; idempotent replays return the key preview without the full secret. Store the full key securely.
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/api-keys" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-key-main-v1" \
-H "Content-Type: application/json" \
-d '{"name":"Acme production key","group_id":123}'
List existing key previews:
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/api-keys" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Disable a key during rotation or abuse response:
curl -X DELETE "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/api-keys/9001" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-key-9001-disable"
A disabled key stops authenticating as soon as the request returns; requests already in flight finish normally. Disabling is permanent — create a new key to resume. Keys have no spend cap of their own: the hard spending limit is the customer organization balance, which you can lower at any time with decrease or set_limit on /allocations.
Usage
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/usage?start_time=2026-07-01T00:00:00Z&end_time=2026-07-08T00:00:00Z" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Usage is reported per customer organization, or per API key on the key's own endpoint below. Each bucket carries burn_usd, request count, total tokens, and the number of API keys active in that bucket; the figures refresh about once a minute.
granularity | Buckets | Default window | Longest window |
|---|---|---|---|
day (default) | one per UTC day | last 7 days | 366 days |
hour | one per UTC hour, start_time rounded down to the hour | last 24 hours | 7 days |
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/usage?granularity=hour&start_time=2026-07-07T00:00:00Z&end_time=2026-07-08T00:00:00Z" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
Any other granularity value returns 400 with reason INVALID_PARTNER_USAGE_GRANULARITY.
To read one key's usage, use the key's own path (api_key_id from the customer's API key list; disabled keys included). It takes the same granularity, start_time and end_time:
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/api-keys/9001/usage?granularity=hour" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
A key that does not belong to that customer returns 404 with reason PARTNER_CUSTOMER_API_KEY_NOT_FOUND. The customer usage endpoint above always reports the whole customer.
End-to-end walkthrough
From an empty customer to a served model call, create first and fund later:
# 1. Create the customer unfunded. It is pending_activation and an invitation goes to contact_email.
curl -X PUT "$QUOTAFLOW_PARTNER_BASE_URL/customers/by-external-id/cust_123" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-cust_123-create-v1" \
-H "Content-Type: application/json" \
-d '{"name":"Acme AI","contact_email":"owner@acme.example","initial_allocation_usd":0,"retail_rate_multiplier":1.0}'
# -> data.customer.id = 101, data.status = "pending_activation", data.activation.invite_url
# 2. Read its status by your own id until it is active.
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/by-external-id/cust_123" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
# -> data.status = "pending_activation", data.activation.invitation_status = "pending"
# 3. Activation: owner@acme.example opens the invitation email (or the invite_url you
# passed on), signs in with that address and accepts. Step 2 then reads "active".
# 4. Fund it on the first purchase.
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/allocations" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-first-purchase" \
-H "Content-Type: application/json" \
-d '{"action":"increase","amount_usd":20}'
# 5. Pick a model group and create the customer's key.
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/groups" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"
curl -X POST "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/api-keys" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN" \
-H "Idempotency-Key: customer-101-key-main-v1" \
-H "Content-Type: application/json" \
-d '{"name":"Acme production key","group_id":123}'
# -> data.key = "qf_..." (shown once)
# 6. Call a model with the customer's key (list its models first with GET /openai/v1/models).
curl https://api.quotaflow.ai/openai/v1/chat/completions \
-H "Authorization: Bearer qf_customer_key" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"Hello"}]}'
# 7. Read what it spent.
curl "$QUOTAFLOW_PARTNER_BASE_URL/customers/101/usage?granularity=hour" \
-H "Authorization: Bearer $QUOTAFLOW_PARTNER_TOKEN"