Vue d’ensemble
Aperçu de l'API
Chaque endpoint de Sofia API v1 avec ses paramètres, ses scopes et ses exemples, et un playground pour tester chacun avec votre propre clé d’API.
URL de base https://api.sofia-post.com/api · Document OpenAPI
Sofia API donne à votre code l’accès à votre compte Sofia : les établissements de vos Workspaces, leurs réseaux connectés, leur médiathèque, leurs publications et leurs statistiques. C’est une API REST en HTTPS ; les requêtes et les réponses sont en JSON. Elle permet de :
- Publier : maintenant, à une date ou en brouillon, sur un ou plusieurs réseaux à la fois.
- Vérifier avant de publier : une simulation vous dit si chaque réseau accepterait la publication, sans rien écrire.
- Gérer les médias : téléversez un fichier ou importez-le depuis une URL dans la médiathèque d’un établissement.
- Suivre les publications : lisez l’état de chaque réseau, son lien permanent ou la raison de l’échec. Un webhook signé peut aussi vous prévenir.
- Lire les statistiques : les derniers chiffres collectés par Sofia pour chaque compte connecté.
- Connecter des réseaux : créez un lien hébergé qui permet au propriétaire de connecter un réseau manquant.
SDK et intégrations
- CLI `sofia` : la CLI, publiée sur npm sous le nom
sofia-post, pour piloter Sofia depuis un terminal. - Serveur MCP : branchez Sofia à vos agents IA.
- Zapier : l’intégration Sofia se connecte avec votre clé personnelle. Elle offre les déclencheurs « Post Published » et « Post Failed », l’action « Create Post » (publier, programmer ou enregistrer un brouillon) et la recherche « Find Post ». Elle n’est pas encore soumise à l’annuaire public de Zapier.
Authentification
Chaque appel porte un jeton Bearer dans l’en-tête Authorization. Sofia relit les droits qui s’y rattachent à chaque appel : une personne retirée d’un Workspace, ou une autorisation révoquée, arrête le jeton dès la requête suivante.
Il y a deux façons de vous authentifier :
| Méthode | Pour | Jeton |
|---|---|---|
| Clé personnelle | Automatiser votre propre compte, scripts, agents | sofia_pk_live_…, valide jusqu’à son expiration |
| OAuth, code d’autorisation + PKCE | Un produit qui agit pour ses utilisateurs, sur le Workspace de leur choix | sofia_live_at_…, une heure, renouvelable |
| OAuth, client credentials | Un de vos serveurs qui automatise un Workspace dont vous êtes propriétaire | sofia_live_at_…, une heure |
Clé API
Si vous automatisez votre propre Workspace, une clé personnelle suffit. Dans l’app Sofia, ouvrez Réglages › API, onglet Accès personnel, et créez une clé. Choisissez son nom, son expiration et ses permissions. La clé n’est affichée qu’une fois : copiez-la et gardez-la dans un coffre de secrets, jamais dans votre code.
Créez une clé dans Réglages › API › Accès personnel. Elle agit pour vous sur tous vos Workspaces : l’établissement que vous nommez détermine le Workspace, et votre rôle dans celui-ci décide de ce que la clé peut faire.
Envoyez la clé dans l’en-tête Authorization, comme jeton Bearer :
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_pk_live_…"`GET /v1/me` vous indique pour qui la clé agit, ses Workspaces et ses permissions.
Un appel sans la permission (le scope) requise répond 403 insufficient_scope.
Jeton OAuth2
Pour un produit SaaS qui agit pour ses utilisateurs sur leurs propres Workspaces, enregistrez un client OAuth : chaque personne autorise votre application sur le Workspace de son choix. Les jetons d’accès OAuth commencent par sofia_live_at_…, durent une heure et s’utilisent de la même façon :
curl https://api.sofia-post.com/api/v1/establishments \
-H "Authorization: Bearer sofia_live_at_…"Vous créez une app à laquelle d’autres comptes Sofia se connectent ? Son flux authorization code avec PKCE, ses client credentials et ses scopes sont décrits dans OAuth2.
URL de base
Chaque endpoint se trouve sous une seule URL de base :
https://api.sofia-post.com/api/v1Limites de débit
Sofia API compte les requêtes sur trois fenêtres : 15 minutes, 24 heures et 30 jours.
| Jeton | 15 minutes | 24 heures | 30 jours |
|---|---|---|---|
| Clé personnelle (par compte) | 150 | 1 000 | 10 000 |
| Client OAuth | 100 | 500 | 15 000 |
| Client OAuth, par personne autorisée (jusqu’à 1 000) | +10 | +25 | +100 |
En-têtes
Chaque réponse vous dit ce qu’il reste, au format IETF :
RateLimit: "15m";r=144;t=43, "24h";r=994;t=13543, "30d";r=9994;t=13543
RateLimit-Policy: "15m";q=150;w=900, "24h";q=1000;w=86400, "30d";q=10000;w=2592000r est le nombre de requêtes restantes, t les secondes avant la réinitialisation de la fenêtre, q le quota et w la fenêtre en secondes.
Quand une fenêtre est pleine
La réponse est 429 rate_limit_exceeded, avec un en-tête Retry-After et la fenêtre épuisée dans details. Attendez ce délai avant de réessayer.
Utiliser l’API efficacement
- Enregistrez un webhook plutôt que d’interroger une publication en boucle.
- Utilisez
limitjusqu’à 100 pour lire les listes en moins de pages. - Mettez en cache ce qui change rarement : plateformes, établissements, comptes connectés.
Erreurs
Chaque erreur répond avec un statut HTTP et la même enveloppe JSON. code est le contrat que lit votre code ; message est écrit pour un humain et peut changer.
{
"error": {
"code": "invalid_request",
"message": "The request is invalid",
"details": [
{ "field": "targets[0].text", "code": "required", "message": "targets[0].text is required" }
],
"docUrl": "https://www.sofia-post.com/developers/errors#invalid_request",
"requestId": "2aab5943-6333-4caf-8478-25a85ebd4abf"
}
}detailsen dit plus : le champ refusé, la raison d’un réseau, la fenêtre de limite épuisée.docUrlmène à l’explication du code sur cette page.requestIdidentifie la requête : communiquez-le au support.
Codes d’erreur
| Statut | Code | Signification |
|---|---|---|
| 400 | invalid_request | Le corps, la requête ou un paramètre est invalide. Un détail par champ refusé : field, code (required, is_string, max…) et message. |
| 400 | invalid_json | Le corps n’est pas du JSON valide. |
| 400 | invalid_cursor | Le curseur n’a pas été renvoyé par Sofia API. |
| 400 | webhook_url_invalid | L’adresse du webhook n’est pas en https, ou mène à une adresse privée. |
| 400 | webhook_event_invalid | Événement de webhook inconnu. |
| 400 | application_name_invalid | Réglages › API : le nom du client est absent ou trop long. |
| 400 | application_profile_invalid | Réglages › API : un champ du profil du client est invalide ; le détail le nomme. |
| 400 | logo_invalid | Réglages › API : le logo est refusé (au moins 180×180 px, PNG, JPEG ou WebP, 2 Mo au plus). |
| 401 | invalid_token | Le jeton est absent, inconnu, expiré ou révoqué, ou la personne derrière a perdu les droits qu’il porte. Voir l’en-tête WWW-Authenticate. |
| 401 | unauthorized | Une authentification est requise. |
| 402 | subscription_required | Une écriture réelle (simulation comprise) exige un abonnement actif ou un essai. |
| 403 | insufficient_scope | Le jeton n’a pas le scope de l’endpoint ; le détail le nomme. |
| 403 | insufficient_role | Votre rôle dans ce Workspace n’autorise pas cette action. |
| 403 | live_mode_required | Une écriture réelle avec un jeton de test. |
| 403 | route_closed_to_applications | Cet endpoint n’est pas ouvert aux applications. |
| 403 | forbidden | L’accès est refusé. |
| 403 | session_required | Réglages › API : cette action demande une session Sofia, pas un jeton. |
| 403 | impersonation_forbidden | Réglages › API : cette action est refusée dans une session empruntée. |
| 403 | workspace_owner_required | Seul le propriétaire du Workspace peut faire cela. |
| 403 | workspace_member_required | Vous n’êtes pas membre de ce Workspace. |
| 404 | route_not_found | Aucun endpoint ne répond à cette méthode et ce chemin. |
| 404 | establishment_not_found | Établissement inconnu, ou hors de portée du jeton. |
| 404 | media_not_found | Média inconnu, ou hors de portée du jeton. |
| 404 | post_not_found | Publication inconnue, ou hors de portée du jeton. |
| 404 | webhook_not_found | Webhook inconnu, ou appartenant à une autre application. |
| 404 | not_found | La ressource n’existe pas. |
| 404 | application_not_found | Réglages › API : client inconnu. |
| 404 | grant_not_found | Autorisation inconnue, ou déjà révoquée. |
| 404 | personal_key_not_found | Réglages › API : clé personnelle inconnue. |
| 404 | consent_not_found | La demande d’autorisation a expiré ou n’existe pas : recommencez depuis l’endpoint d’autorisation. |
| 405 | method_not_allowed | Cette méthode n’est pas autorisée sur cet endpoint. |
| 409 | post_in_flight | Un réseau est en train d’envoyer la publication : réessayez la suppression un instant plus tard. |
| 409 | post_not_editable | Un réseau a déjà envoyé la publication : elle ne peut plus être modifiée. |
| 409 | account_already_connected | Ce réseau est déjà connecté à l’établissement. |
| 409 | webhook_limit_reached | L’application a déjà 10 webhooks. |
| 409 | idempotency_in_progress | La première requête avec cette Idempotency-Key est encore en cours. |
| 409 | conflict | La requête entre en conflit avec l’état actuel de la ressource. |
| 409 | scope_removal_forbidden | Réglages › API : une permission d’un client peut être ajoutée mais pas retirée. |
| 409 | public_client_has_no_secret | Réglages › API : un client public n’a pas de secret. |
| 413 | payload_too_large | Le fichier dépasse la limite de téléversement. |
| 415 | unsupported_media_type | Type de contenu non pris en charge. |
| 422 | post_refused | Un réseau, l’établissement ou votre forfait refuserait la publication. Un détail par raison, chacun avec son message. |
| 422 | media_import_refused | Le média n’a pas pu être importé depuis cette URL ; le détail donne la raison. |
| 422 | media_upload_refused | Le fichier téléversé n’est pas une image ou une vidéo prise en charge. |
| 422 | platform_not_offered | Ce réseau n’est pas proposé à la connexion. |
| 422 | idempotency_key_reused | Cette Idempotency-Key a servi avec un corps différent. |
| 422 | unprocessable_entity | La requête est comprise mais ne peut pas être traitée. |
| 429 | rate_limit_exceeded | Une fenêtre de limite est pleine : attendez Retry-After. |
Raisons d’une publication refusée
Un post_refused liste chaque raison dans details[].code :
| Code | Signification |
|---|---|
unsupported_platform | Ce réseau n’est pas pris en charge. |
duplicate_platform | Le même réseau est nommé deux fois. |
unsupported_format | Ce réseau ne propose pas ce format. |
caption_too_long | Le texte est trop long (max, actual). |
too_many_hashtags | Trop de hashtags (max, actual). |
too_many_mentions | Trop de mentions (max, actual). |
too_many_thread_posts | Le fil est trop long (max, actual). |
too_many_media | Trop de médias (max, actual). |
media_mix_refused | Photos et vidéos ne peuvent pas être mélangées sur ce réseau. |
media_required | Ce réseau exige une photo ou une vidéo. |
media_type_refused | Ce réseau n’accepte pas ce type de média. |
scheduled_in_past | scheduledAt est dans le passé. |
scheduled_at_invalid | Une publication programmée exige un scheduledAt futur. |
account_not_connected | Aucun compte connecté sur ce réseau pour cet établissement. |
account_ambiguous | Plusieurs comptes sont connectés sur ce réseau : nommez-en un avec accountId. |
media_not_found | Le média n’est pas dans la médiathèque de cet établissement. |
media_url_invalid | L’URL du média doit être une adresse https publique. |
platform_unavailable | Ce réseau n’est pas encore ouvert sur Sofia. |
publication_quota_exceeded | Le quota de publications de votre forfait est atteint. |
Gérer les erreurs
- Branchez votre logique sur
code, jamais surmessage. - Réessayez un
429aprèsRetry-After, et un5xxavec un backoff exponentiel. - Ne réessayez jamais un
4xxà l’identique : corrigez d’abord la requête. - Journalisez
requestIdà chaque échec.
Pagination
Les listes (GET /v1/posts, GET /v1/media) sont renvoyées les plus récentes d’abord, une page à la fois. limit fixe la taille de page, de 1 à 100 (20 par défaut).
{
"data": [ … ],
"nextCursor": "WyIyMDI2LTEwLTI3VDE1OjExOjU5LjAxN1oiLCIx…"
}Passez nextCursor comme cursor pour lire la page suivante. Quand nextCursor est absent, vous avez atteint la fin.
let cursor;
do {
const url = new URL('https://api.sofia-post.com/api/v1/posts');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
handle(page.data);
cursor = page.nextCursor;
} while (cursor);Un curseur est opaque : ne le construisez ni ne le modifiez jamais. Un curseur que Sofia n’a pas renvoyé répond 400 invalid_cursor.
Idempotence
Une erreur réseau peut vous laisser dans le doute sur la création d’une publication. Envoyez un en-tête Idempotency-Key avec POST /v1/posts et POST /v1/media, et réessayez avec la même clé : Sofia répond ce qu’elle a répondu la première fois, et ne crée rien en double.
curl -X POST https://api.sofia-post.com/api/v1/posts \
-H "Authorization: Bearer $SOFIA_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d @post.json- Utilisez une nouvelle clé aléatoire (un UUID) pour chaque nouvelle publication, et la même clé pour chaque nouvel essai de cette publication.
- La même clé avec un corps différent répond
422 idempotency_key_reused. - Tant que la première requête est en cours, la même clé répond
409 idempotency_in_progress: réessayez un instant plus tard.
Mode test et simulation
Simulation. Ajoutez ?dry_run=true à POST /v1/posts : Sofia exécute les contrôles d’une vraie publication (les règles de chaque réseau, le compte connecté, les médias, la date et votre forfait) sans rien enregistrer ni envoyer. Le détail des réponses est sur la page Publier.
Mode test. Une application créée en mode test (jetons sofia_test_at_…) lit votre vrai Workspace mais n’y écrit jamais : ses publications sont simulées et jugées selon les mêmes règles, et toute écriture réelle répond 403 live_mode_required. Les clés et clients créés dans Réglages › API sont en mode live ; utilisez la simulation pour vérifier sans publier.
OAuth
/v1/oauth/authorizeDémarrer une autorisationÀ ouvrir dans le navigateur. Un client inconnu ou une redirect_uri non enregistrée renvoie un 400 ; sinon, redirection vers l’écran de consentement de Sofia.GET/.well-known/oauth-authorization-server/api/v1/oauthLire les métadonnées du serveurRenvoie les métadonnées du serveur d’autorisation OAuth 2.0 de Sofia API (RFC 8414), pour qu’un client découvre les endpoints et les options prises en charge.POST/v1/oauth/tokenÉmettre un jeton d’accèsÉchange des client_credentials, un authorization_code avec PKCE ou un refresh_token à usage unique contre un jeton opaque de 3600 s pour un Workspace.Compte
Plateformes
Établissements
/v1/establishmentsLister les établissementsListe les établissements du Workspace du jeton. Utilisez leurs identifiants comme establishmentId pour publier, lister les médias ou lire les statistiques.GET/v1/establishments/{id}/accountsLister les comptes connectésListe les réseaux connectés à un établissement, sans jeton ni secret de réseau, seulement de quoi nommer un compte et le choisir comme accountId.POST/v1/establishments/{id}/connection-linksCréer un lien de connexionRenvoie un lien hébergé où la personne autorise un réseau auprès de Sofia, sans qu’aucun jeton de réseau ne vous parvienne. Valable 30 minutes, en live.GET/v1/establishments/{id}/insightsLire les statistiquesRenvoie les derniers chiffres valides collectés par Sofia pour chaque compte connecté. Un compte pas encore collecté affiche awaiting, jamais zéro.Médias
/v1/mediaLister les médiasListe la médiathèque d’un établissement, les plus récents d’abord, page par page, avec des URL signées pour la fenêtre en cours. Fonctionne en test et en live.POST/v1/mediaImporter ou téléverser un médiaAjoute à la médiathèque d’un établissement un fichier (multipart) ou une URL https que Sofia récupère. Exige une application live et un abonnement actif.GET/v1/media/{id}Lire un médiaRenvoie un média de la médiathèque avec une URL signée valable pour la fenêtre en cours, pour que vous puissiez réutiliser son identifiant dans une publication.DELETE/v1/media/{id}Retirer un médiaRetire un média de la médiathèque de son établissement, comme le fait l’app. Le fichier n’est pas détruit et une publication programmée garde son visuel.Publications
/v1/postsLister les publicationsListe les publications des établissements accessibles au jeton, les plus récentes d’abord, filtrées par établissement, statut, dates et auteur, page par page.POST/v1/postsPublier une publicationUne application live publie aussitôt, programme ou enregistre un brouillon ; en test, elle simule. Avec ?dry_run=true, tout est vérifié et rien n’est écrit.GET/v1/posts/{id}Lire une publicationRenvoie une publication avec l’état de chaque réseau, son lien et son erreur éventuelle. En live, toute publication du Workspace ; en test, une simulée.PATCH/v1/posts/{id}Modifier une publicationAvant l’envoi sur un réseau, remplace les targets, fixe scheduledAt ou passe la publication de draft à scheduled et inversement. Champs omis conservés.DELETE/v1/posts/{id}Supprimer une publicationSupprime une publication comme l’app : rien de programmé n’est envoyé et une publication en ligne est retirée de son réseau. En test, rien de réel à supprimer.Webhooks
/v1/webhooksLister les webhooksListe les webhooks de l’application sans leur secret de signature, qui n’est renvoyé qu’à l’enregistrement du webhook.POST/v1/webhooksEnregistrer un webhookEnregistre une URL https que Sofia appelle avec un événement JSON signé, rejoué jusqu’à cinq fois en cas d’échec. Secret affiché une fois ; 10 webhooks max.DELETE/v1/webhooks/{id}Supprimer un webhookSupprime un webhook. Il ne reçoit plus rien, pas même les envois qui attendaient encore une nouvelle tentative, et l’appel ne renvoie aucun corps.POST/v1/webhooks/{id}/testEnvoyer un événement de testEnvoie un événement webhook.test signé comme les autres, sans attendre le récepteur, pour que vous vérifiiez votre signature. Cinq au maximum par minute.