Skip to content

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:

  1. The account must be partner-enabled by Protypa. Until then these routes answer 403 with code: "PARTNER_NOT_ENABLED". Contact Protypa to join the programme.
  2. A key must carry the partner scope explicitly. A key without it is refused with 403 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

  1. Protypa
  2. Your partner account
  3. Client workspace
  4. Client key
  5. Client's application
A client workspace is owned by you. Its usage counts against your account: one bill from Protypa to you, one bill from you to each client.

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

DirectionHow it is computedWhere 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
MarginYour 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?

API key with the partner scope, or sessionpartner

The 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.

Example response
{ "enabled": true }

Errors

  • 403scope_deniedThe key does not carry the partner scope.

GET/partner/clients

List your client workspaces with this month usage.

API key with the partner scope, or sessionpartner

Request 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.

Example response
{
"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

  • 403PARTNER_NOT_ENABLEDThe account is not a partner.
  • 403scope_deniedThe key does not carry the partner scope.

POST/partner/clients

Create a client workspace.

API key with the partner scope, or sessionpartner

Creates 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

  • namestringrequired
    1 to 80 characters.
  • adminEmailstring
    Email of the client manager to invite as administrator.
  • ratePer100Centsinteger
    Your resale rate per 100 questions, in euro cents (0 to 1,000,000), or null.

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.

Example response
{
"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.

API key with the partner scope, or sessionpartner

Send 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

  • idstringrequired
    The client workspace id returned when the client was created.

Request body

  • namestring
    New name, 1 to 80 characters.
  • ratePer100Centsinteger
    Your price per 100 questions beyond the included ones.
  • seatRateCentsinteger
    Your price per active seat per month (up to 1,000,000).
  • includedQuestionsPerSeatinteger
    Questions included per seat before the per-100 rate applies.
  • suspendedboolean
    Cut 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.

Example response
{ "client": { "id": "ws_9d41", "name": "Cabinet Durand" } }

POST/partner/clients/:id/keys

Issue an API key to a client application.

API key with the partner scope, or sessionpartner

A 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

  • idstringrequired
    The client workspace id returned when the client was created.

Request body

  • namestringrequired
    1 to 80 characters, for your own reference.
  • scopesstring[]required
    Non-empty. At least one capability scope; not partner.
  • monthlyQuestionLimitinteger
    Calendar-month ceiling in questions. Verifier controls do not count.
  • ratePerMinuteinteger
    Requests per minute, up to 100,000.
  • expiresAtstring
    ISO 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.

Example response
{
"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.

API key with the partner scope, or sessionpartner

Never the secret. monthQuestions is the count the ceiling is enforced on, so the gauge and a 402 never disagree.

Path parameters

  • idstringrequired
    The 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.

Example response
{
"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.

API key with the partner scope, or sessionpartner

The client application loses access at its next call.

Path parameters

  • idstringrequired
    The client workspace id returned when the client was created.
  • keyIdstringrequired
    The key id.

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.

API key with the partner scope, or sessionpartner

Path parameters

  • idstringrequired
    The client workspace id returned when the client was created.

Query parameters

  • monthsintegerdefault 6
    How 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.

Example response
{
"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.

API key with the partner scope, or sessionpartner

The 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

  • idstringrequired
    The client workspace id returned when the client was created.

Query parameters

  • monthstringdefault current month
    Calendar month as YYYY-MM, for example 2026-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.

Example response
{
"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.

API key with the partner scope, or sessionpartner

Your 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

  • idstringrequired
    The client workspace id returned when the client was created.

Query parameters

  • monthsintegerdefault 6
    1 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.

API key with the partner scope, or sessionpartner

Not allowed for a month that has not started. Send amountCents only when what you received differs from the statement.

Path parameters

  • idstringrequired
    The client workspace id returned when the client was created.
  • monthstringrequired
    YYYY-MM.

Request body

  • paidbooleanrequired
    True to mark paid, false to undo.
  • amountCentsinteger
    Amount 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.

API key with the partner scope, or sessionpartner

Semicolon-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

  • monthstringdefault previous month
    Calendar month as YYYY-MM, for example 2026-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.csv

Responses

text/csv; charset=utf-8, attachment clients-<month>.csv.

GET/partner/wholesale

What this account owes Protypa.

API key with the partner scope, or sessionpartner

Current 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

  • monthstringdefault current month
    Calendar month as YYYY-MM, for example 2026-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).

Example response
{
"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.

API key with the partner scope, or sessionpartner

Every 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).

Example response
{
"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.

API key with the partner scope, or sessionpartner

A 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.

API key with the partner scope, or sessionpartner

Path parameters

  • idstringrequired
    The device id from GET /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.

API key with the partner scope, or sessionpartner

Charges the invoice's exact amount. Refused if the invoice is already paid or void.

Path parameters

  • idstringrequired
    The invoice id from GET /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.

Example response
{ "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.

API key with the partner scope, or sessionpartner

Path parameters

  • idstringrequired
    The invoice id.

Query parameters

  • session_idstringrequired
    The 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.

API key with the partner scope, or sessionpartner

Opens 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.

Example response
{ "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.

API key with the partner scope, or sessionpartner

Query parameters

  • session_idstringrequired
    The 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.

API key with the partner scope, or sessionpartner

Forgets 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

Create, key, check
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 administrator
const { 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 end
const 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.

Related