Limits and failures

Everything in the partner plane fails closed. A guard that is unsure refuses, an unset budget refuses rather than meaning "unlimited", and a missing configuration is a 503 rather than a pass. This page is the complete list of what can refuse you and why.

Errors carry one shape throughout:

{"detail": {"error": "partner_key_invalid", "message": "Missing or invalid X-Partner-Key."}}

Authentication

Every /api/partner/* call resolves your key first. Four outcomes:

Status error Meaning
503 provisioning_not_configured the plane is off in this environment, or no partner key exists at all anywhere. Not something you can fix from your side
401 partner_key_invalid header absent, or the key is unknown, malformed, or revoked. All of these answer identically — telling a prober that a key exists but is revoked tells them a key exists
403 partner_suspended the key is valid; your partner record is suspended. Distinguishable on purpose — you already proved possession of a live key, so you learn nothing you did not have
429 rate_limited over your per-partner rate limit. Carries a Retry-After header

Revocation takes effect on the next request. There is no propagation delay and no cache. Other live keys are unaffected, which is what makes rotation safe — see Rotating.

Suspension is the operator's kill switch and it is reversible. It destroys nothing: accounts you already provisioned keep working, because they are ordinary accounts and were never your property. While suspended you can still revoke keys; you cannot mint them.

Failed-auth throttling

Failed authentications are counted per IP: 40 failures per 60-second window by default. Over the cap you get 429 with a Retry-After, and the cap is checked before the database is touched — key-spraying cannot be amplified. A successful authentication clears your IP's history, so one mistyped key never penalises you.

Rate limits

Surface Default Keyed on Window
The whole partner plane 120 requests/minute your partner id, not your IP 60s
GET /api/register/{token} (pre-flight) 30/minute the end user's IP 60s
POST /api/register 10/minute the end user's IP 60s
HTTP/1.1 429 Too Many Requests
Retry-After: 27
{"detail": {"error": "rate_limited",
            "message": "Rate limit exceeded for this partner key; slow down and retry shortly."}}

Being keyed on the partner rather than the address is deliberate: you are a server, your address is stable and shared by all your customers, and an IP bucket would be either useless or an outage.

::: tip The partner cap is a runaway-loop guard, not a business limit. Provisioning is a bulk, scripted operation by design — an integrator onboarding a customer base does thousands in an afternoon. If you are hitting 120/minute in normal operation, back off on Retry-After and keep going; you are not doing anything unsupported. :::

The registration caps are tighter than the pre-flight for a reason: every call that gets past the token check reaches our identity service, so this is also what stops a leaked registration URL becoming a stream of upstream writes while someone guesses passwords.

Size and shape limits

What Limit
external_ref 1-128 chars, ^[A-Za-z0-9._:-]+$
display_label 120 chars
referral_code 64 chars
metadata 4096 bytes serialised. 422 metadata_too_large
Purchase reference 1-128 chars, same charset as external_ref
Purchase note 200 chars
Key label 80 chars
Partner slug 2-40 chars, letters/digits/-/_
limit on list endpoints 1-500, default 100
Accounts shown in your panel 200 — use GET /api/partner/accounts for the complete list

The metadata cap is generous for the plan hints and tenant ids a partner actually stores, and small enough that the column cannot become free storage on a write we do not charge for.

Money limits

Guard Default Owned by
Per-call purchase ceiling $2,000 operator
Rolling-24h purchase ceiling $10,000 operator — you cannot raise it
Per-account seed ceiling $25 operator
Seeded accounts per rolling 24h 200 operator
Total seed budget you set it. 0 refuses you
24h seed budget you set it. 0 refuses, ≤ total you
Your funding account balance reality

The two velocity limits are operator-owned on purpose: the scenario they cover is your own credential is what went wrong, and a limit a compromised credential can raise is not a limit.

Every one of these is enforced on spend as well as on write, so a settings row edited directly in the database still cannot over-spend. Full refusal tables are in Billing.

An empty wallet refuses rather than going negative. The debit is all-or-nothing. An under-funded partner seeds nothing and buys nothing; nothing is ever partially applied.

Your customer can revoke you

An account you provisioned was never your property. Once its owner has registered it, their settings page shows a Connected apps section naming you, with a Revoke access button.

After they use it:

What Before After
GET /api/partner/accounts/{owner_id} 200 404 account_not_found
GET /api/partner/accounts listed absent entirely
POST …/credit works 404
POST …/registration-link works 404
Your panel's customer list listed the row disappears
Their account, characters, balance and data theirs unchanged, still theirs

::: warning It is a 404, not a 403, and you are not told the account still exists. That is deliberate and it is the same posture as every id-scoped lookup here: "not yours" and "does not exist" must be indistinguishable, because account ids are derived from references you supply and a partner guessing another partner's references is the exact attack.

The practical consequence: a 404 on an account you are certain you provisioned means one of two things and you cannot tell which from the API — the customer revoked you, or you have the wrong id. Only an operator can see the difference. If a customer's integration stops working with no other explanation, that is the first thing to ask us about. :::

The row is not deleted. That means the external_ref is not freed — re-provisioning it returns the same (now severed) account rather than creating a new one, because the id derivation is deterministic. If a customer comes back after revoking, they reconnect from your end; you do not re-provision.

Revoking twice is a 200. An account with no partner link answers the revoke with 404 no_partner_link, which is honest rather than a silent success.

What is deliberately not available to you

Not oversights. Each is a decision, and asking for it will get the same answer:

You cannot Why
See a customer's email address, ever there is none before registration, and after it the address is theirs
See a customer's balance or plan through the partner plane those live behind a different credential; provisioning must not become a second path to money data
Read or spend any account's balance with your partner key the key resolves to a partner and nothing else. It names no account in any request
Set your own funding account 403 owner_id_is_operator_only. A partner who could choose which account money comes out of could drain a stranger's
Rename or unsuspend yourself, or change your slug the slug is stamped into the audit trail of every account you provisioned
Re-mint a registration link for a registered account 409 already_registered, permanently
Rotate your provisioning salt rotating it would silently start minting a second account for every reference you ever provisioned. If a salt is believed compromised the answer is a new partner
Delete an account or a key row keys keep a revoked_at; accounts keep the relationship. Both stay answerable after an incident
Request a payout from the partner API that is the Affiliate tab, on the same account and the same money

Idempotency, in one place

Three writes are idempotent, each on a key you choose. Retry all three freely.

Call Key Replay behaviour
POST /partner/accounts external_ref 200 instead of 201, same account
POST …/credit reference 200, the original row, "replayed": true
DELETE …/keys/{id} the key id 200, unchanged timestamp

Seeding is idempotent independently: one seed per account, ever, enforced by a uniqueness constraint regardless of how many times you retry the provision.

Nothing else is. In particular, minting a registration link is not idempotent — each call replaces the previous token.

The gaps, stated plainly

::: danger The sandbox is isolated, not simulated. There is no test mode, no test key prefix, no simulated billing and no dry-run anywhere. Every conversation spends real money at real prices, in the sandbox as much as in production — the difference is that we fund the sandbox, within caps, so evaluating us costs you nothing.

Provisioning, registration links, account reads and purchases are free in both — they touch no model — so you can exercise the whole account lifecycle at no cost anywhere. It is only conversations that charge. In production, keep your integration testing on one account you watch. :::

::: warning A partner context cannot reach FluidCloud. The connected-app credential makes you the owner of an account inside FluidTalk, but it carries no user assertion that can be replayed to FluidCloud. Anything that needs a Cloud tenant refuses — including Cloud-backed photo features.

This is a real boundary, not a switch waiting to be flipped. If your product depends on those features, your customer has to claim the account and use them signed in. Raise it with your Fluid contact before you commit to a launch date. :::