API partenaires
L’API partenaires permet à un partenaire de mener l’ensemble du parcours commercial depuis ses propres systèmes : enregistrer une affaire dès qu’une opportunité est créée, amener un client sur la plateforme lorsqu’elle est gagnée, et rapprocher les relevés de marge de sa propre comptabilité.
Pour ce que ce parcours signifie sur le plan commercial (périodes de protection, conflits, calcul de la marge), lisez d’abord le guide du Programme partenaires. Cette page en décrit la mécanique.
URL de base :
https://frem.sh/_app/api/v1/_app. frem.sh/api/v1/... est l’API propre de Forgejo, une surface différente avec des jetons différents. Un appel partenaire envoyé là-bas n’échouera pas de manière évidente : c’est simplement Forgejo qui y répondra.Authentification
Les endpoints partenaires acceptent un jeton d’accès personnel fremforge comme jeton bearer :
curl -H "Authorization: Bearer $FF_TOKEN" \
https://frem.sh/_app/api/v1/partner/meDeux scopes régissent l’accès :
| Scope | Accorde |
|---|---|
partner:read | /partner/me, /partner/customers, /partner/deals (GET), /partner/statements |
partner:write | Enregistrement d’affaires, demande de conditions entreprise, création de clients |
Une intégration en lecture seule (un tableau de bord, une tâche de rapprochement) ne devrait détenir que partner:read.
Obtenir un jeton
Créez-en un depuis le portail partenaire sous Votre accès, ou à partir d’un jeton existant disposant du scope tokens:manage :
curl -X POST -H "Authorization: Bearer $FF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"crm-integration","scopes":["partner:read","partner:write"],"ttl_days":90}' \
https://frem.sh/_app/api/v1/auth/tokens{ "token": "ffp_...", "id": "0192...", "expires_at": "2026-11-14T09:12:00Z" }ttl_days vaut 30 par défaut et est plafonné à 90 : une intégration de longue durée a donc besoin d’un plan de rotation plutôt que d’un jeton qu’on configure une fois pour toutes. La valeur du jeton n’est affichée qu’une seule fois. Voir Gestion des jetons pour la rotation et la révocation.
Deux façons dont un appel peut être refusé
Ces deux cas ont des significations différentes, et il vaut la peine de les distinguer dans votre gestion des erreurs :
403 not_a_partner: le jeton est valide, mais son propriétaire n’est pas un utilisateur partenaire. Généralement, ce n’est pas le bon jeton.403 insufficient-scope: la bonne identité, mais sans le scope requis. Recréez le jeton avecpartner:write.
Aucune de ces deux orthographes n’est une coquille de cette page : not_a_partner utilise un trait de soulignement, insufficient-scope un trait d’union. Comparez avec la chaîne exacte.
Les données partenaires sont délibérément inter-tenants : ces endpoints ne sont pas limités à une organisation, et le propre tenant d’un partenaire est exclu des tenants qu’il peut par ailleurs administrer.
GET /partner/me
L’identité au nom de laquelle vous agissez, et si l’enregistrement d’affaires est actuellement suspendu pour vous.
curl -H "Authorization: Bearer $FF_TOKEN" \
https://frem.sh/_app/api/v1/partner/me{
"partner_id": "3f2a...",
"partner_name": "Nordic Resell ApS",
"username": "anna",
"agreement_status": "active"
}Un agreement_status à paused permet toujours de s’authentifier et sert toujours toutes les lectures. La suspension est un contrôle anti-abus sur l’enregistrement, pas un verrouillage : ne la traitez pas comme un échec d’authentification.
GET /partner/customers
Les clients qui vous sont actuellement attribués.
{ "customers": [ { "tenant_id": "9c1e...", "org_slug": "acme" } ] }La marge court à partir de la date d’attribution : un client attribué tardivement ne rapporte rien pour les mois qui précèdent.
GET /partner/deals
Toutes les affaires que vous avez enregistrées, de la plus récente à la plus ancienne.
{
"deals": [
{
"id": "7b40...",
"end_customer": "Acme A/S",
"country": "DK",
"status": "approved",
"conflict_state": "clear",
"tenant_id": null,
"protection_expires_at": "2026-11-14T00:00:00Z"
}
]
}protection_expires_at marque la fin de la période de 90 jours ouverte par l’enregistrement. Nous vous envoyons un e-mail 14 jours et 3 jours avant son expiration.
Un conflict_state à flagged signifie qu’un autre partenaire a enregistré la même société. Nous tranchons ces cas sur la base des preuves d’activité commerciale, jamais selon qui a enregistré en premier, et nous ne divulguons pas l’identité de l’autre partenaire.
POST /partner/deals
Enregistre un prospect et ouvre la période de protection. Nécessite partner:write.
curl -X POST -H "Authorization: Bearer $FF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"end_customer_legal_name": "Acme A/S",
"end_customer_country": "DK",
"end_customer_registration_number": "12345678",
"end_customer_contact_email": "cto@acme.example",
"estimated_seats": 25,
"expected_close_date": "2026-11-01",
"sales_activity_evidence": "Demo held 2026-08-12; POC agreed with their CTO."
}' \
https://frem.sh/_app/api/v1/partner/dealsObligatoires : end_customer_legal_name, end_customer_country (ISO 3166-1 alpha-2, en majuscules).
{ "id": "7b40...", "flagged": false, "pinned_price_list_version": "2026-06" }| Statut | Signification |
|---|---|
201 | Enregistrée |
400 | Refusée : le corps indique quel contrôle a échoué |
429 | Plafond quotidien d’enregistrements atteint |
flagged: true est un 201, pas une erreur. L’affaire est enregistrée ; un autre partenaire a enregistré la même société et nous trancherons. Ne relancez pas : relancer consomme votre quota quotidien et ne change rien.Renseignez soigneusement sales_activity_evidence. C’est sur ces preuves qu’un conflit est tranché.
POST /partner/deals/{dealId}/enterprise-request
Demande une tarification négociée par utilisateur avant de créer le client. Nécessite partner:write.
curl -X POST -H "Authorization: Bearer $FF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":"Needs 400 seats and annual invoicing; competing against a US vendor."}' \
https://frem.sh/_app/api/v1/partner/deals/7b40.../enterprise-request{ "ok": true, "recorded": true }Répond toujours 200. recorded: false signifie que la demande n’a pas été prise en compte, et ce délibérément sans distinguer « déjà demandé » de « pas votre affaire ».
La première écriture l’emporte : demander deux fois ne déplace pas la date enregistrée et ne remplace pas la note. La demande n’est acceptée que tant que l’affaire est approuvée ou gagnée et qu’aucun client n’existe encore ; ensuite, le tenant est bien réel et sa conversion est une action de l’opérateur.
POST /partner/deals/{dealId}/customer
Amène sur la plateforme le client d’une affaire que vous avez gagnée. Nécessite partner:write.
curl -X POST -H "Authorization: Bearer $FF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"org_slug": "acme",
"display_name": "Acme A/S",
"admin_email": "cto@acme.example"
}' \
https://frem.sh/_app/api/v1/partner/deals/7b40.../customerObligatoires : org_slug (en minuscules, compatible DNS, de 3 à 30 caractères ; il devient frem.sh/<slug> et ne peut pas être modifié), display_name, admin_email.
Cet appel crée l’organisation, invite l’administrateur par e-mail et vous attribue le client, afin que la marge coure dès sa première facture.
Le client démarre en période d’essai. La formule n’est délibérément pas un paramètre : la choisir reviendrait à accorder des conditions commerciales. La réponse contient billing_setup_url, où le client ajoute un moyen de paiement et passe à une formule payante ; le même lien lui est envoyé par e-mail.
Refusé lorsque l’affaire ne vous appartient pas, n’est ni approuvée ni gagnée, a déjà un client, ou lorsque l’enregistrement est suspendu pour vous. 429 lorsque le plafond quotidien de création de clients est atteint.
GET /partner/statements
Vos relevés de marge, de la période la plus récente à la plus ancienne.
{
"statements": [
{
"id": "aa19...",
"period_start": "2026-07-01",
"period_end": "2026-07-31",
"currency": "EUR",
"margin_eur": "1284.50",
"total_invoiceable_eur": "1284.50",
"sent_at": "2026-08-01T06:00:00Z",
"partner_invoice_ref": "INV-2026-114",
"paid_at": null
}
]
}Les relevés sont établis le 1er de chaque mois pour le mois calendaire précédent.
Le règlement suit l’ordre sent_at → partner_invoice_ref → paid_at. Rien n’est payé avant qu’une référence de facture partenaire existe, car c’est votre facture qui constitue la pièce sur laquelle porte le paiement. Le paiement intervient 30 jours après sa réception.
Un parcours complet
Enregistrement depuis un CRM dès qu’une opportunité est créée, puis création du client lorsqu’elle est gagnée :
BASE=https://frem.sh/_app/api/v1
AUTH="Authorization: Bearer $FF_TOKEN"
# 1. Register as soon as the opportunity exists — this is what starts protection.
DEAL=$(curl -sSf -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d '{"end_customer_legal_name":"Acme A/S","end_customer_country":"DK",
"sales_activity_evidence":"Discovery call 2026-08-12"}' \
"$BASE/partner/deals")
DEAL_ID=$(printf '%s' "$DEAL" | jq -r .id)
# `flagged` is informational, not a failure — carry on, do not retry.
printf '%s' "$DEAL" | jq -r 'if .flagged then "conflict registered; we will resolve it" else "protected" end'
# 2. When it is won, create the customer.
curl -sSf -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d '{"org_slug":"acme","display_name":"Acme A/S","admin_email":"cto@acme.example"}' \
"$BASE/partner/deals/$DEAL_ID/customer" | jq -r .billing_setup_url
# 3. Reconcile, as strings.
curl -sSf -H "$AUTH" "$BASE/partner/statements" \
| jq -r '.statements[] | "\(.period_start) \(.margin_eur) \(.currency) paid=\(.paid_at // "no")"'Erreurs
Les endpoints partenaires utilisent la forme application/problem+json commune à toute la plateforme. L’URL type renvoie vers une page qui décrit l’échec précis : voir Erreurs de l’API.
La spécification complète
Ces endpoints font partie du document OpenAPI publié, sous le tag Partner :
La spécification est générée à partir du service en cours d’exécution et fait foi lorsque cette page et elle divergent.