API de facturation
Récupérez l'usage et le coût de votre propre workspace dans vos propres systèmes, sans ouvrir la console. Une clé de facturation est un en lecture seule identifiant limité à un seul workspace - elle peut lire les données de facturation, rien de plus.
Une clé de facturation peut lire chaque requête enregistrée dans son workspace, y compris les clés depuis supprimées ou révoquées, mais elle ne peut appeler aucun modèle, ni créer, modifier ou supprimer de clé API. L'utiliser ailleurs renvoie 401 wrong_key_kind.
Créer une clé de facturation
- Ouvrez Console → Clés d'API de facturation (propriétaire du workspace uniquement). Nommez-la, restreignez-la éventuellement à une liste blanche d'IP ou définissez une expiration, puis copiez la clé - elle commence par
sk-syn-bill-et ne s'affiche qu'une seule fois. - Appelez les endpoints ci-dessous en l'utilisant comme jeton Bearer. Révoquez-la à tout moment depuis la même page - la révocation l'invalide immédiatement et n'affecte rien d'autre dans le workspace.
- Une clé est liée à son créateur : s'il n'est plus le propriétaire du workspace, elle cesse de fonctionner et renvoie
403 key_owner_not_authorized.
Authentification
Chaque appel nécessite Authorization: Bearer sk-syn-bill-... envoyé à l'URL de base ci-dessous. Une clé de facturation ne fonctionne que sur ces endpoints, et ces endpoints n'acceptent qu'une clé de facturation - toute autre combinaison renvoie 401 wrong_key_kind.
Authorization: Bearer sk-syn-bill-... URL de base: https://synthorai.io/api/v1/billing
La portée couvre tout le workspace : chaque clé d'inférence qu'il contient, y compris celles supprimées - leurs lignes historiques restent interrogeables. api_key_id et model ne peuvent que restreindre le résultat, jamais l'élargir ; chaque requête est ancrée sur le workspace propre de la clé.
Chaque requête s'exécute sur la réplique en lecture, jamais sur la primaire. Un processus sans réplique configurée répond 503 replica_unavailable plutôt que de basculer silencieusement.
Champs de tokens : suivent la convention OpenAI - prompt_tokens est le total des tokens d'entrée et inclut déjà chaque lecture et écriture de cache ci-dessous, et completion_tokens inclut déjà reasoning_tokens. Aucun des deux ne s'ajoute aux totaux - ce sont des décompositions, pas des tokens supplémentaires.
La part de lecture de cache de prompt_tokens est cached_tokens ; la part d'écriture de cache est cache_write_tokens, elle-même divisée sur les lignes de /records en cache_write_5m_tokens et cache_write_1h_tokens selon le TTL.
Champs monétaires : cost_usd est le montant réellement facturé. byok_list_price_usd est ce qu'aurait coûté une requête BYOK au prix catalogue - 0 pour une requête non BYOK. Il n'existe pas de list_price_usd nulle part dans cette API.
Ici, "erreur" désigne toute requête qui a atteint l'API et a été rejetée - un 4xx comme 402 (solde insuffisant), 403 ou 429, ainsi qu'un échec en amont. Seuls nos propres rejets d'infrastructure interne sont exclus, exactement comme dans la console. Les rejets sont enregistrés au plus une fois par clé, code de statut et minute sur chaque serveur : les nombres de 402, 403 et 429 sont donc des minimums ; les requêtes réussies et les montants facturés sont complets.
GET /usage - usage agrégé
GET /usage
Des lignes pré-agrégées par heure ou par jour - pour la facturation et les tableaux de bord. Elles viennent de l'agrégat horaire, complété des requêtes écrites depuis sa dernière ingestion, si bien que les buckets récents n'ont pas un cycle d'ingestion entier de retard.
| Paramètre | Type | Description |
|---|---|---|
start* | string | Début de la plage : un horodatage RFC 3339 ou une date telle que 2026-09-01. |
end | string | Fin de la plage. Par défaut, l'heure actuelle. |
granularity | string | Taille du bucket : hour ou day. Par défaut, day. |
group_by | string | Dimensions selon lesquelles répartir les lignes : api_key, model, ou les deux (tout sous-ensemble). Par défaut, les deux. |
api_key_id | integer | Filtre sur un ou plusieurs id de clé API. Répétable ; ne fait que restreindre, jamais élargir. |
model | string | Filtre sur un ou plusieurs id de modèle. Répétable. |
limit | integer | Lignes par page. Par défaut 1000, plafonné à 5000. |
cursor | string | Curseur de pagination opaque, repris du meta.next_cursor de la page précédente. |
Dans chaque ligne, api_key_id et api_key_name n'apparaissent que lorsque group_by inclut api_key, et model uniquement lorsqu'il inclut model. avg_latency_ms peut être null quand un bucket n'a aucun échantillon de latence.
Exemple de requête
curl "https://synthorai.io/api/v1/billing/usage?start=2026-09-01&end=2026-09-24&granularity=day" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{
"data": [
{
"bucket_start": "2026-09-23T00:00:00Z",
"bucket_end": "2026-09-24T00:00:00Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"requests": 1280,
"success_requests": 1265,
"error_requests": 15,
"errors_4xx": 12,
"errors_429": 2,
"errors_5xx": 1,
"prompt_tokens": 512000,
"completion_tokens": 98000,
"cached_tokens": 210000,
"cache_write_tokens": 8000,
"reasoning_tokens": 4000,
"cost_usd": 4.1732,
"byok_list_price_usd": 0,
"byok_requests": 0,
"avg_latency_ms": 812,
"final": true
}
],
"meta": {
"generated_at": "2026-09-24T08:00:00Z",
"data_complete_until": "2026-09-24T06:00:00Z",
"timezone": "UTC",
"currency": "USD",
"next_cursor": null,
"has_more": false
}
} GET /records - lignes par requête
GET /records
Une ligne par requête, destinée à une synchronisation incrémentale vers votre propre base de données. Synchronisez par cursor, jamais par le temps, et dédupliquez les lignes reçues par record_id - voir "Éviter les trous temporels" ci-dessous pour la raison.
| Paramètre | Type | Description |
|---|---|---|
cursor | string | Curseur de synchronisation opaque (chaîne), repris du meta.next_cursor de l'appel précédent. Fournissez exactement l'un des deux, cursor ou start ; il n'a aucun plafond de plage. |
start | string | Début de la plage (horodatage RFC 3339 ou date). Fournissez exactement l'un des deux, cursor ou start ; une plage start/end est plafonnée à 31 jours, tandis que cursor n'a aucun plafond de plage. |
end | string | Fin de la plage. Par défaut, l'heure actuelle. |
api_key_id | integer | Filtre sur un ou plusieurs id de clé API. Répétable ; ne fait que restreindre, jamais élargir. |
model | string | Filtre sur un ou plusieurs id de modèle. Répétable. |
limit | integer | Lignes par page. Par défaut 500, plafonné à 1000. |
Exemple de requête
curl "https://synthorai.io/api/v1/billing/records?start=2026-09-24T00:00:00Z&limit=500" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{
"data": [
{
"record_id": "rec_8f2a91c4",
"request_id": "req_9f3c2b1a",
"recorded_at": "2026-09-24T05:58:31Z",
"completed_at": "2026-09-24T05:58:29Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"status": "success",
"status_code": 200,
"prompt_tokens": 812,
"completion_tokens": 194,
"cached_tokens": 512,
"cache_write_tokens": 64,
"cache_write_5m_tokens": 64,
"cache_write_1h_tokens": 0,
"cost_usd": 0.00412,
"byok_list_price_usd": 0,
"is_byok": false,
"is_stream": true,
"duration_ms": 1340,
"ttft_ms": 210
}
],
"meta": {
"next_cursor": "eyJvIjoiODgxNDA5MCJ9",
"has_more": false,
"window_final": true,
"safe_until": "2026-09-24T05:58:31Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"data_complete_until": "2026-09-24T05:58:00Z"
}
} | Paramètre | Description |
|---|---|
next_cursor | Curseur pour le prochain appel. Toujours présent. |
has_more | Indique si un nouvel appel immédiat avec ce curseur renverrait davantage de lignes. |
window_final | true uniquement si vous avez fourni un end et qu'il est à ou avant safe_until - la plage demandée est alors garantie complète. Absent ou false sinon. |
safe_until | Le recorded_at le plus récent qu'un appelant peut considérer comme définitif ; les lignes plus récentes que cela sont encore retenues. |
latest_recorded_at | Le recorded_at le plus récent parmi toutes les lignes, définitives ou non. |
data_complete_until | Même valeur que celle renvoyée par /freshness - jusqu'où le rollup horaire est complet, pour recouper avec /usage. |
Base temporelle : /usage regroupe par completed_at (quand la requête s'est terminée). Le filtre start/end de /records se base sur recorded_at (quand la ligne a été écrite, ce qui peut retarder par rapport à l'achèvement sous forte charge). Pour recouper /records avec /usage, regroupez vous-même les lignes synchronisées par completed_at, pas par recorded_at.
GET /records/export - export en flux
GET /records/export
Mêmes filtres que /records, diffusés en JSON délimité par des retours à la ligne (application/x-ndjson) - conçu pour un grand rattrapage plutôt qu'une liste paginée. Un seul export peut être diffusé par workspace à la fois ; un second reçoit 429 too_many_concurrent_requests.
Exemple de requête
curl -N "https://synthorai.io/api/v1/billing/records/export?cursor=eyJvIjoiODgxNDAzMiJ9" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{"record_id":"rec_8f2a91c4","request_id":"req_9f3c2b1a","model":"claude-sonnet-5","status":"success","cost_usd":0.00412}
{"record_id":"rec_8f2a91c5","request_id":"req_9f3c2b1b","model":"claude-sonnet-5","status":"success","cost_usd":0.00398}
{"_meta":{"next_cursor":"eyJvIjoiODgxNDA5MSJ9","rows":2,"complete":true,"window_final":true,"generated_at":"2026-09-24T06:00:02Z"}} La dernière ligne du flux est un _meta objet :
| Paramètre | Description |
|---|---|
next_cursor | Curseur pour le prochain appel d'export. |
rows | Nombre d'enregistrements écrits dans ce flux. |
complete | false signifie que le flux s'est arrêté avant d'épuiser la plage - reprenez avec cursor=next_cursor. |
window_final | Même signification que sur /records : true uniquement si le end de l'export est à ou avant safe_until. |
generated_at | Moment où cette ligne a été écrite. |
stopped_because | Présent quand complete vaut false : row_cap, scan_cap (le flux a parcouru sa plage d'id maximale sans se remplir - fréquent avec un filtre étroit), time_cap, query_error ou window_not_final. |
GET /summary - statistiques préliminaires et anomalies
GET /summary
Totaux, meilleurs modèles et meilleures clés par coût, et un taux d'erreur pour la plage, plus une anomalies liste (high_error_rate, rate_limited, data_not_final, rollup_delayed), chacune portant une gravité info ou warning et un message lisible. Les chiffres d'une plage non finalisée peuvent encore changer - voir plus bas.
| Paramètre | Type | Description |
|---|---|---|
start | string | Début de la plage. Par défaut, 30 jours avant end. |
end | string | Fin de la plage. Par défaut, l'heure actuelle. |
api_key_id | integer | Filtre sur un ou plusieurs id de clé API. Répétable ; ne fait que restreindre, jamais élargir. |
model | string | Filtre sur un ou plusieurs id de modèle. Répétable. |
models_count et keys_count donnent les totaux derrière top_models et top_keys, qui ne listent que les 20 premiers par coût au maximum. La plage est par défaut les 30 jours avant end lorsque start est omis.
Exemple de requête
curl "https://synthorai.io/api/v1/billing/summary?start=2026-09-17&end=2026-09-24" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{
"data": {
"totals": { "requests": 48210, "cost_usd": 132.55, "error_rate": 0.021 },
"models_count": 6,
"keys_count": 3,
"top_models": [ { "model": "claude-sonnet-5", "cost_usd": 88.10, "byok_list_price_usd": 0 } ],
"top_keys": [ { "api_key_id": 42, "api_key_name": "prod-backend", "cost_usd": 61.30, "byok_list_price_usd": 0 } ],
"anomalies": [
{ "type": "data_not_final", "severity": "info", "message": "The last 2 hours of this range are not yet final." }
]
},
"meta": {
"generated_at": "2026-09-24T06:00:02Z",
"data_complete_until": "2026-09-24T04:00:00Z"
}
} GET /freshness - degré de complétude des données
GET /freshness
Renvoie data_complete_until, latest_recorded_at, safe_until et pipeline_lag_seconds - interrogez-le avant de considérer une plage tout juste récupérée comme définitive. safe_until est le recorded_at le plus récent qu'un appelant peut considérer comme complet ; des lignes plus récentes peuvent encore arriver.
Exemple de requête
curl "https://synthorai.io/api/v1/billing/freshness" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{
"data": {
"data_complete_until": "2026-09-24T04:00:00Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"safe_until": "2026-09-24T05:58:31Z",
"pipeline_lag_seconds": 47
},
"meta": { "generated_at": "2026-09-24T06:00:02Z" }
} GET /balance - ce que l'espace de travail peut dépenser maintenant
GET /balance
Renvoie le solde disponible actuel de l'espace de travail - le même montant que dans la console. Conçu pour l'interrogation périodique : toutes les 30 à 60 secondes suffisent. Il a sa propre limite de débit, donc l'interroger n'entame pas le budget des endpoints de données, et il répond même quand le solde est à zéro.
Exemple de requête
curl "https://synthorai.io/api/v1/billing/balance" \
-H "Authorization: Bearer sk-syn-bill-..." Exemple de réponse
{
"data": {
"available_usd": 1284.37,
"debt_usd": 0,
"source": "live",
"voucher": null,
"scheduled_credits": [
{ "amount_usd": 250, "release_at": "2026-10-01T00:00:00Z" }
],
"scheduled_credit_total_usd": 250
},
"meta": { "generated_at": "2026-09-24T06:00:02Z", "currency": "USD" }
} | Paramètre | Description |
|---|---|
available_usd | Le solde général sur lequel les requêtes sont facturées en ce moment. Jamais en dessous de 0. Il peut remonter brièvement quand des requêtes en cours sont réglées ; pour les dépenses, utilisez /usage ou /records. |
debt_usd | Le solde dû : la consommation au-delà du montant crédité. 0 s'il n'y a rien à payer. Une recharge le règle en premier ; seul le reste s'ajoute à available_usd. |
source | live : lu dans le registre en temps réel. delayed : le registre était injoignable, et la valeur vient d'une copie en base qui peut avoir environ 30 secondes de retard. |
voucher | Crédit promotionnel réservé à la génération d'images (applies_to), sur les modèles listés et jusqu'à expires_at ; les autres requêtes n'utilisent que available_usd, et ce crédit n'en fait pas partie. null s'il n'y en a pas ; active vaut false une fois expiré. |
scheduled_credits | Crédits déjà convenus versés par tranches (amount_usd, release_at) ; chaque tranche est ajoutée au solde dans les 10 minutes environ suivant release_at, pas avant. scheduled_credit_total_usd en est la somme. |
Erreurs
Chaque erreur est {"error": {"type", "message", "hint"}}:
{
"error": {
"type": "range_too_large",
"message": "the requested range exceeds this endpoint's cap",
"hint": "narrow start/end to at most 31 days, or sync /records with a cursor instead"
}
} | Type | Statut HTTP | Signification |
|---|---|---|
invalid_parameter | 400 | Un paramètre de requête est manquant, malformé ou hors plage. |
range_too_large | 400 | La plage temporelle ou la fenêtre d'id demandée dépasse ce que l'endpoint autorise ; le hint indique le plafond concerné. |
start_too_recent | 400 | Une fenêtre start/end commence après safe_until, là où sa borne de début n'est pas encore stabilisée. Réessayez après le délai de l'en-tête Retry-After, ou synchronisez avec un cursor. |
wrong_key_kind | 401 | L'identifiant n'est pas une clé de facturation, ou une clé de facturation a été utilisée hors de ces endpoints. |
authentication_error | 401 | La clé est manquante, invalide, expirée, ou bloquée par sa liste blanche d'IP. |
key_owner_not_authorized | 403 | Le créateur de cette clé n'est plus le propriétaire du workspace. Les clés de facturation sont réservées au propriétaire et cessent de fonctionner dès que la propriété change de main ; le nouveau propriétaire doit créer la sienne. |
rate_limited | 429 | Plus de 60 requêtes par minute sur ce workspace. Réessayez après le délai indiqué dans l'en-tête Retry-After. |
too_many_concurrent_requests | 429 | Plus de 3 requêtes simultanées sur cet espace de travail, ou un autre export déjà en cours. |
replica_unavailable | 503 | Ce processus n'a aucune réplique en lecture configurée ; l'API ne bascule jamais vers la primaire. |
query_timeout | 503 | La requête a dépassé le délai d'expiration de 10 secondes ou l'échéance de 15 secondes. Restreignez la plage et réessayez. |
internal_error | 500 | Une erreur serveur inattendue. Réessayez, et signalez-la si elle persiste. |
Limites de débit et plafonds
| Garde-fou | Valeur |
|---|---|
| Limite de débit | 60 requêtes par minute par workspace, une fenêtre glissante partagée entre toutes les instances. |
| Concurrence | 3 requêtes simultanées par espace de travail sur chaque serveur, et un flux d'export par espace de travail sur chaque serveur. |
| Interrogation du solde | /balance : 60 requêtes par minute et 2 en parallèle par espace de travail, comptées à part des autres endpoints. |
| Plafonds de plage | /usage : 31 jours en granularité hour, 366 jours en day. /summary : jusqu'à 366 jours. /records et l'export : jusqu'à 31 jours. |
| Plafonds de pagination | /usage renvoie jusqu'à 5 000 lignes par page, /records jusqu'à 1 000. L'export diffuse des pages de 2 000 et s'arrête à 1 000 000 de lignes - reprenable avec cursor. Il s'arrête aussi après avoir parcouru 2 000 000 d'id d'enregistrement (stopped_because=scan_cap) et règle son rythme sur la base de données ; reprenez avec cursor. |
| Mise en cache | Chaque réponse porte Cache-Control: no-store. |
Jamais renvoyé : channel_id, upstream_request_id, usage_raw, other, content, ip_address, user_agent, cost_detail.
Éviter les trous temporels
Les requêtes sont écrites dans le log de façon asynchrone après leur fin (quelques secondes normalement, plus en cas de retard accumulé), et le rollup horaire lu par /usage et /summary est ingéré par lots. Les heures les plus récentes sont toujours un peu incomplètes. /usage et /summary ajoutent aussi les requêtes écrites depuis la dernière ingestion (meta.live_tail vaut true) ; si l'agrégat a trop de retard pour cela, meta.live_tail vaut false et les buckets récents sont partiels.
data_complete_until est le plus tôt des deux points suivants : le filigrane du rollup horaire moins son retard de pipeline actuel, ou la requête la plus ancienne pas encore intégrée au rollup - moins une marge de sécurité de 30 minutes, arrondi à l'heure inférieure.
Un bucket final est stable, à une exception près : la tâche de réconciliation quotidienne peut encore corriger une heure dans les 48 heures si elle détecte une dérive. /records reste toujours la source de vérité pour les chiffres exacts d'une requête.
Chaque ligne de /usage porte un indicateur final : true , vrai dès que son bucket se termine avant ou à data_complete_until (valeur renvoyée par /freshness). Un bucket final ne change plus jamais.
- Agrégats: upsertez chaque ligne dans votre propre stockage selon (bucket_start, api_key_id, model), et récupérez à nouveau tout bucket tant qu'il est final: false. Ne calculez jamais un delta en soustrayant un ancien total d'un nouveau.
- Lignes brutes: synchronisez par cursor, jamais par le temps. Conservez next_cursor et reprenez à partir de là au prochain appel, et dédupliquez les lignes reçues par record_id (un id opaque stable par ligne) au cas où une page reprise redélivrerait la même ligne. Les lignes de moins d'environ 5 minutes sont retenues - safe_until est l'heure de la ligne la plus récemment enregistrée moins 5 minutes - si bien qu'une ligne encore en cours d'écriture ne peut jamais se glisser avant une position de curseur déjà distribuée, et rien ne tombe dans un trou. Pour compter les requêtes, comptez les request_id distincts.
- Plage bornée: si vous interrogez une plage start/end au lieu de synchroniser par cursor, ne la considérez complète que lorsque window_final est true (il n'est true que si vous avez fourni un end et qu'il est à ou avant safe_until). Si window_final est false, rappelez plus tard avec la même plage, ou passez à une synchronisation par cursor. Une fenêtre doit commencer au plus tard à safe_until ; un début plus tardif renvoie 400 start_too_recent avec un en-tête Retry-After.
Intégration recommandée
- Facturation nocturne: appelez /usage une fois par jour avec granularity=day, et récupérez à nouveau tout bucket que votre dernière exécution voyait encore comme final: false.
- Détail par requête: appelez /records chaque heure avec cursor réglé sur le dernier next_cursor que vous avez stocké, continuez à paginer tant que has_more est true, et dédupliquez les lignes reçues par record_id.
- Tableaux de bord: appelez /summary pour la plage visible plutôt que d'agréger /records vous-même - elle porte déjà la liste des anomalies.
Besoin de générer ou de gérer des clés API classiques par programmation plutôt que de lire des données de facturation ? Voir Clés de provisioning.