Partner API
For approved integrators: create client workspaces, issue each client a restricted API key, and read the usage and statements you re-invoice from, all over the API.
The Partner API is for integrators who run Subsidia for several client organisations from one account. It is not the general developer API: it manages the client workspaces you own, not your own data. Everything here is also available in the app (Partner space); each step exists over the API so a client can be provisioned without a browser.
For the bigger picture (architecture, one key per client, what to hand to a client's legal team) read Building on Subsidia for your clients.
Two gates
Every /partner/* route is behind both:
- The account must be partner-enabled by Protypa. Until then these routes answer
403withcode: "PARTNER_NOT_ENABLED". Contact Protypa to join the programme. - A key must carry the
partnerscope explicitly. A key without it is refused with403 scope_denied, even on a partner-enabled account, and so is a legacy full-access key created before scopes existed (otherwise the desktop app's device key could create clients). A signed-in session in the app does not need a scope.
Create the partner key in the Subsidia console with the partner scope only: it manages your fleet and nothing else.
The model
- Protypa
- Your partner account
- Client workspace
- Client key
- Client's application
A client workspace is separate from the others: every read is scoped to it, and a dossier can be reserved to named people. You own it, so you can reach its documents, its access journal and its keys. Every member of a client workspace sees a notice that you run it, and a client administrator can acknowledge it; the acknowledgement date is visible to you in the client list (managedAckAt).
Billing model
| Direction | How it is computed | Where you see it |
|---|---|---|
| Protypa to you (wholesale) | Per active seat across all your client workspaces, plus questions consumed that month, priced on a marginal volume grid (each tier at its own rate, like income tax), with an optional monthly platform minimum. Questions are weighted by model class. Verifier checks are counted as controls, apart from questions. | GET /partner/wholesale (the live grid is in the response), GET /partner/invoices |
| You to your client (resale) | Whatever you set per client: a rate per 100 questions, an optional price per seat, and optional questions included per seat. Subsidia never bills your client; you do. | GET /partner/clients, GET /partner/clients/:id/statement, GET /partner/usage.csv |
| Margin | Your resale total minus the wholesale price for the same seats and questions. | marginCents in a client statement |
Amounts are in euro cents, excluding tax. An invoice left unpaid past the grace period (7 days) suspends your clients' access (nothing is deleted) until it is paid. Customer-facing copy talks about questions, not tokens: a question is a unit of model usage with a fixed ceiling per answer, see Rate limits and quotas.
Endpoints
GET/partner/status
Is this account a partner?
partnerThe only partner route open to a key that has the scope on a non-partner account. Use it to check your setup before anything else.
Request examples
curl "https://api.subsidia.protypa.fr/partner/status" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Whether the programme is enabled.
{ "enabled": true }Errors
- 403
scope_deniedThe key does not carry thepartnerscope.
GET/partner/clients
List your client workspaces with this month usage.
partnerRequest examples
curl "https://api.subsidia.protypa.fr/partner/clients" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
One entry per client, oldest first, and totals. monthAmountCents is null while a client has no resale rate; totals.monthAmountCents is null unless every client has one.
{ "clients": [ { "id": "ws_9d41", "name": "Cabinet Durand", "createdAt": "2026-08-12T09:00:00.000Z", "members": 4, "pendingInvitations": 1, "apiKeys": 2, "monthCalls": 812, "monthQuestions": 640, "monthCostUsd": 0.91, "lastActivityAt": "2026-10-09T07:58:00.000Z", "ratePer100Cents": 500, "seatRateCents": 1900, "includedQuestionsPerSeat": 400, "seats": 5, "monthAmountCents": 9500, "managedAckAt": "2026-08-14T13:20:00.000Z", "suspendedAt": null, "unpaidMonths": [] } ], "totals": { "clients": 1, "monthCalls": 812, "monthQuestions": 640, "monthCostUsd": 0.91, "monthAmountCents": 9500 }}Errors
- 403
PARTNER_NOT_ENABLEDThe account is not a partner. - 403
scope_deniedThe key does not carry thepartnerscope.
POST/partner/clients
Create a client workspace.
partnerCreates a workspace owned by you. With adminEmail, that person is invited as the client's administrator; without it the workspace exists but nobody there can sign in yet (your own API keys still work through the keys you mint).
Request body
namestringrequired1 to 80 characters.adminEmailstringEmail of the client manager to invite as administrator.ratePer100CentsintegerYour resale rate per 100 questions, in euro cents (0 to 1,000,000), ornull.
Request examples
curl -X POST "https://api.subsidia.protypa.fr/partner/clients" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{ "name": "Cabinet Durand", "adminEmail": "direction@cabinet-durand.example", "ratePer100Cents": 500}'Responses
The client, and the invitation if one was sent.
{ "client": { "id": "ws_9d41", "name": "Cabinet Durand", "createdAt": "2026-10-09T09:00:00.000Z" }, "invitation": { "email": "direction@cabinet-durand.example", "role": "admin" }}PATCH/partner/clients/:id
Rename a client, set its resale terms, or suspend it.
partnerSend at least one field. Money fields are non-negative whole numbers (euro cents or questions); null clears one. suspended: true refuses every API key of the client with 403 client_suspended without revoking any, and false restores them: a pause button for a client who has not paid.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.
Request body
namestringNew name, 1 to 80 characters.ratePer100CentsintegerYour price per 100 questions beyond the included ones.seatRateCentsintegerYour price per active seat per month (up to 1,000,000).includedQuestionsPerSeatintegerQuestions included per seat before the per-100 rate applies.suspendedbooleanCut off, or restore, all API keys of this client.
Request examples
curl -X PATCH "https://api.subsidia.protypa.fr/partner/clients/$ID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{ "seatRateCents": 1900, "includedQuestionsPerSeat": 400, "ratePer100Cents": 500}'Responses
The updated client.
{ "client": { "id": "ws_9d41", "name": "Cabinet Durand" } }POST/partner/clients/:id/keys
Issue an API key to a client application.
partnerA restricted key for the client's own software. It must name at least one capability scope (engine, knowledge, reflex, light, webhooks, proof), may add readonly, and can never carry partner. Optional ceilings protect you from a runaway integration: monthlyQuestionLimit answers 402 API_KEY_BUDGET_EXCEEDED once reached (counted from usage logs, so it can lag a burst by a few seconds), ratePerMinute answers 429 with Retry-After.
The raw key is in the response once.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.
Request body
namestringrequired1 to 80 characters, for your own reference.scopesstring[]requiredNon-empty. At least one capability scope; notpartner.monthlyQuestionLimitintegerCalendar-month ceiling in questions. Verifier controls do not count.ratePerMinuteintegerRequests per minute, up to 100,000.expiresAtstringISO 8601 date-time after which the key stops working.
Request examples
curl -X POST "https://api.subsidia.protypa.fr/partner/clients/$ID/keys" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{ "name": "Portail Durand", "scopes": [ "engine", "knowledge", "proof" ], "monthlyQuestionLimit": 2000, "ratePerMinute": 60}'Responses
The key (key) and its settings.
{ "key": "sk_live_3f9a...c2", "id": "key_71ab", "name": "Portail Durand", "prefix": "sk_live_3f9a1c07", "scopes": ["engine", "knowledge", "proof"], "monthlyQuestionLimit": 2000, "ratePerMinute": 60, "expiresAt": null, "note": "Store this key now — it will not be shown again."}GET/partner/clients/:id/keys
List the active keys of a client.
partnerNever the secret. monthQuestions is the count the ceiling is enforced on, so the gauge and a 402 never disagree.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.
Request examples
curl "https://api.subsidia.protypa.fr/partner/clients/$ID/keys" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The client and its active keys.
{ "client": { "id": "ws_9d41", "name": "Cabinet Durand", "createdAt": "2026-08-12T09:00:00.000Z" }, "keys": [ { "id": "key_71ab", "name": "Portail Durand", "prefix": "sk_live_3f9a1c07", "scopes": ["engine", "knowledge", "proof"], "monthlyQuestionLimit": 2000, "ratePerMinute": 60, "usageCount": 1840, "lastUsedAt": "2026-10-09T07:58:00.000Z", "createdAt": "2026-08-12T09:10:00.000Z", "expiresAt": null, "monthQuestions": 640 } ]}DELETE/partner/clients/:id/keys/:keyId
Revoke a client key.
partnerThe client application loses access at its next call.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.keyIdstringrequiredThe keyid.
Request examples
curl -X DELETE "https://api.subsidia.protypa.fr/partner/clients/$ID/keys/$KEYID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Revoked.
GET/partner/clients/:id/usage
Monthly usage of a client, oldest first.
partnerPath parameters
idstringrequiredThe client workspace id returned when the client was created.
Query parameters
monthsintegerdefault6How many calendar months, 1 to 24, empty months included.
Request examples
curl "https://api.subsidia.protypa.fr/partner/clients/$ID/usage?months=3" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Calls, questions and estimated provider cost in USD per month.
{ "client": { "id": "ws_9d41", "name": "Cabinet Durand", "createdAt": "2026-08-12T09:00:00.000Z" }, "months": [ { "month": "2026-08", "calls": 120, "questions": 98, "costUsd": 0.12 }, { "month": "2026-09", "calls": 701, "questions": 566, "costUsd": 0.77 }, { "month": "2026-10", "calls": 812, "questions": 640, "costUsd": 0.91 } ]}GET/partner/clients/:id/statement
A client monthly statement: what to bill, and your margin.
partnerThe document you re-invoice from. It computes the client total at your resale terms (seats, included questions, questions beyond them), next to Protypa's wholesale price for the same usage and the resulting margin. controls is the number of Verifier checks that month, shown apart from questions. For a closed month, seats are counted at month end; for the running month, as they stand now (seatsBasis).
Path parameters
idstringrequiredThe client workspace id returned when the client was created.
Query parameters
monthstringdefaultcurrent monthCalendar month asYYYY-MM, for example2026-09. Anything else is a 400.
Request examples
curl "https://api.subsidia.protypa.fr/partner/clients/$ID/statement?month=2026-09" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The statement. Fields that depend on a resale rate you have not set are null.
{ "month": "2026-09", "clientName": "Cabinet Durand", "issuer": { "name": "Integrator SAS", "email": "billing@integrator.example" }, "seatsBasis": "month-end", "seats": 5, "seatRateCents": 1900, "seatAmountCents": 9500, "questions": 566, "controls": 128, "includedQuestions": 2000, "billableQuestions": 0, "ratePer100Cents": 500, "questionAmountCents": 0, "totalCents": 9500, "wholesale": { "seatCents": 1000, "questionCents100": 449, "seatAmountCents": 5000, "questionAmountCents": 2541, "totalCents": 7541 }, "marginCents": 1959}Notes
The statement also carries a usage breakdown object. The wholesale figures in the example are illustrative: the live grid is returned by GET /partner/wholesale.
GET/partner/clients/:id/payments
Which months a client has paid you.
partnerYour own ledger, newest first: the statement total of each month, whether it is due (finished, with something to collect) and when you marked it paid. Subsidia never bills your client; this only records what you tell it.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.
Query parameters
monthsintegerdefault61 to 24.
Request examples
curl "https://api.subsidia.protypa.fr/partner/clients/$ID/payments" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
{ months: [...] }, one entry per month.
PUT/partner/clients/:id/payments/:month
Mark a month paid or unpaid.
partnerNot allowed for a month that has not started. Send amountCents only when what you received differs from the statement.
Path parameters
idstringrequiredThe client workspace id returned when the client was created.monthstringrequiredYYYY-MM.
Request body
paidbooleanrequiredTrue to mark paid, false to undo.amountCentsintegerAmount actually received, in euro cents.
Request examples
curl -X PUT "https://api.subsidia.protypa.fr/partner/clients/$ID/payments/$MONTH" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{ "paid": true}'Responses
The recorded payment (payment).
GET/partner/usage.csv
One line per client for a month, to re-invoice from.
partnerSemicolon-separated with a UTF-8 byte-order mark and CRLF line ends, so a French spreadsheet opens it in columns. A total line closes the file. Columns: mois, client, appels, questions, tarif_pour_100_questions_eur, montant_ht_eur, sieges, tarif_par_siege_eur, montant_sieges_ht_eur, questions_incluses, questions_facturables, montant_questions_ht_eur. montant_ht_eur is the client total (seats plus billable questions).
Query parameters
monthstringdefaultprevious monthCalendar month asYYYY-MM, for example2026-09. Anything else is a 400.
Request examples
curl "https://api.subsidia.protypa.fr/partner/usage.csv?month=2026-09" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -o clients-2026-09.csvResponses
text/csv; charset=utf-8, attachment clients-<month>.csv.
GET/partner/wholesale
What this account owes Protypa.
partnerCurrent seats across every workspace you run and the month's questions, priced on the standing wholesale grid. The response includes the live grid (marginal tiers) and the weights per modelClasses, so you can forecast. controls and controlAmountCents show Verifier checks apart from questions.
Query parameters
monthstringdefaultcurrent monthCalendar month asYYYY-MM, for example2026-09. Anything else is a 400.
Request examples
curl "https://api.subsidia.protypa.fr/partner/wholesale?month=2026-10" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The wholesale statement (abridged).
{ "month": "2026-10", "seats": 12, "seatCents": 1000, "seatAmountCents": 12000, "questions": 4120, "questionCents100": 449, "questionAmountCents": 18499, "controls": 128, "controlAmountCents": 0, "minimumCents": 0, "totalCents": 30499, "tiers": [{ "from": 0, "upTo": 50000, "cents100": 449, "questions": 4120, "amountCents": 18499 }], "grid": [{ "upTo": 50000, "cents100": 449 }, { "upTo": null, "cents100": 49 }]}Notes
Illustrative numbers. The grid is set by Protypa and can change: read it from this endpoint rather than hard-coding it.
GET/partner/invoices
Your wholesale invoices and what is outstanding.
partnerEvery invoice Protypa sent you, the outstanding amount (sent, not yet paid), whether automatic debit is on, whether your clients are currently suspended for non-payment, and the grace period in days.
Request examples
curl "https://api.subsidia.protypa.fr/partner/invoices" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Invoices and billing state (the invoice objects are abridged here).
{ "invoices": [], "outstandingCents": 0, "autoDebit": true, "autoDebitSince": "2026-09-02T10:00:00.000Z", "suspended": false, "graceDays": 7, "platformFeeCents": 0}GET/partner/devices
Every paired desktop install across your clients.
partnerA support view: which machines run Subsidia at your clients, their state (ok, attention, offline, expiring, never-seen, revoked) and version, without asking each client. A desktop is paired by signing the app in with your account and choosing the client workspace.
Request examples
curl "https://api.subsidia.protypa.fr/partner/devices" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The fleet.
DELETE/partner/devices/:id
Remove a paired install.
partnerPath parameters
idstringrequiredThe device id fromGET /partner/devices.
Request examples
curl -X DELETE "https://api.subsidia.protypa.fr/partner/devices/$ID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Removed.
Paying Protypa
Paying the wholesale invoice goes through Stripe Checkout, so these routes return a URL to open in a browser rather than a receipt. The two confirm routes are what your redirect page calls after Stripe sends the person back; they are idempotent and do not wait for Stripe's own notification.
POST/partner/invoices/:id/pay
Open a Checkout session for one invoice.
partnerCharges the invoice's exact amount. Refused if the invoice is already paid or void.
Path parameters
idstringrequiredThe invoice id fromGET /partner/invoices.
Request examples
curl -X POST "https://api.subsidia.protypa.fr/partner/invoices/$ID/pay" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Open url in a browser.
{ "sessionId": "cs_live_a1...", "url": "https://checkout.stripe.com/c/pay/cs_live_a1..." }GET/partner/invoices/:id/confirm
Confirm a payment after the Stripe redirect.
partnerPath parameters
idstringrequiredThe invoice id.
Query parameters
session_idstringrequiredThe Checkout session id Stripe appended to your return URL.
Request examples
curl "https://api.subsidia.protypa.fr/partner/invoices/$ID/confirm?session_id=cs_live_a1" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The payment state after verification.
POST/partner/auto-debit
Authorise automatic debit of the monthly invoice.
partnerOpens a Stripe Checkout session in setup mode where you save a card or SEPA account once. Each monthly wholesale invoice is then debited at its exact amount.
Request examples
curl -X POST "https://api.subsidia.protypa.fr/partner/auto-debit" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Open url in a browser.
{ "sessionId": "cs_live_b2...", "url": "https://checkout.stripe.com/c/setup/cs_live_b2..." }GET/partner/auto-debit/confirm
Confirm the debit authorisation after the Stripe redirect.
partnerQuery parameters
session_idstringrequiredThe Checkout session id.
Request examples
curl "https://api.subsidia.protypa.fr/partner/auto-debit/confirm?session_id=cs_live_b2" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
The authorisation is saved.
DELETE/partner/auto-debit
Stop the automatic debit.
partnerForgets the saved payment method. Invoices are then paid by hand from Checkout.
Request examples
curl -X DELETE "https://api.subsidia.protypa.fr/partner/auto-debit" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"Responses
Forgotten.
Provision a client end to end
const base = 'https://api.subsidia.protypa.fr'const headers = { Authorization: 'Bearer ' + process.env.SUBSIDIA_PARTNER_KEY, // sk_live_... with the partner scope 'Content-Type': 'application/json',}
// 1. A workspace for the client, with its manager invited as administratorconst { client } = await fetch(base + '/partner/clients', { method: 'POST', headers, body: JSON.stringify({ name: 'Cabinet Durand', adminEmail: 'direction@cabinet-durand.example', ratePer100Cents: 500 }),}).then((r) => r.json())
// 2. A restricted key for the client's own application (shown once)const { key } = await fetch(base + '/partner/clients/' + client.id + '/keys', { method: 'POST', headers, body: JSON.stringify({ name: 'Portail Durand', scopes: ['engine', 'proof'], monthlyQuestionLimit: 2000, ratePerMinute: 60 }),}).then((r) => r.json())// hand "key" to the client's application: it can call the Gateway and the Verifier, nothing else
// 3. At month endconst statement = await fetch(base + '/partner/clients/' + client.id + '/statement?month=2026-09', { headers }).then((r) => r.json())console.log(statement.totalCents, statement.controls, statement.marginCents)Frequently asked
Why does my key get scope_denied on a partner account?
The key was created without the partner scope, or before scopes existed. Create a new key and tick partner. A client key can never carry it.
What does suspending a client do?
Every API key of that workspace answers 403 client_suspended until you set suspended: false. Nothing is revoked or deleted, so it is reversible with no re-issuing of keys.
Does the client see what I charge?
No. Statements and rates are yours. The client sees the usage its own keys and members generate.
Are Verifier checks billed?
They are counted per client as controls, apart from questions, and appear on statements. At the time of writing no price is attached to them.