Partner-API
Mit der Partner-API wickelt ein Partner den gesamten kommerziellen Ablauf aus seinen eigenen Systemen ab: ein Geschäft registrieren, sobald eine Verkaufschance angelegt wird, einen Kunden auf die Plattform bringen, wenn das Geschäft gewonnen ist, und Margenabrechnungen mit der eigenen Buchhaltung abgleichen.
Was der Ablauf kommerziell bedeutet – Schutzfenster, Konflikte, wie die Marge berechnet wird –, liest du zuerst im Leitfaden zum Partnerprogramm. Diese Seite beschreibt die Technik.
Basis-URL:
https://frem.sh/_app/api/v1/_app. frem.sh/api/v1/... ist die eigene API von Forgejo, eine andere Schnittstelle mit anderen Tokens. Ein Partner-Aufruf, der dort landet, schlägt nicht offensichtlich fehl – er wird einfach von Forgejo beantwortet.Authentifizierung
Partner-Endpunkte erwarten ein persönliches Zugriffstoken von fremforge als Bearer-Token:
curl -H "Authorization: Bearer $FF_TOKEN" \
https://frem.sh/_app/api/v1/partner/meZwei Scopes steuern den Zugriff:
| Scope | Erlaubt |
|---|---|
partner:read | /partner/me, /partner/customers, /partner/deals (GET), /partner/statements |
partner:write | Geschäfte registrieren, Enterprise-Konditionen anfragen, Kunden anlegen |
Eine Integration, die nur liest – ein Dashboard, ein Abgleichsjob –, sollte ausschließlich partner:read haben.
Ein Token bekommen
Erzeuge eines im Partnerportal unter Dein Zugang oder mit einem vorhandenen Token, das den Scope tokens:manage hat:
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 ist standardmäßig 30 und auf höchstens 90 begrenzt. Eine langlebige Integration braucht also einen Rotationsplan statt eines Tokens, das man einmal einträgt und vergisst. Der Wert des Tokens wird nur einmal angezeigt. Rotation und Widerruf beschreibt die Token-Verwaltung.
Zwei Arten, wie ein Aufruf abgelehnt wird
Die beiden bedeuten Verschiedenes, und es lohnt sich, sie in deiner Fehlerbehandlung zu unterscheiden:
403 not_a_partner– das Token ist gültig, aber sein Inhaber ist kein Partner-Nutzer. Meist das falsche Token.403 insufficient-scope– die richtige Identität, aber der Scope fehlt. Erzeuge das Token neu mitpartner:write.
Keine der beiden Schreibweisen ist ein Tippfehler auf dieser Seite: not_a_partner verwendet einen Unterstrich, insufficient-scope einen Bindestrich. Prüfe auf genau diese Zeichenkette.
Partnerdaten sind bewusst mandantenübergreifend: Diese Endpunkte sind nicht auf eine Organisation beschränkt, und der eigene Mandant eines Partners ist von den Mandanten ausgenommen, die er sonst verwalten kann.
GET /partner/me
In wessen Namen du handelst und ob die Registrierung von Geschäften für dich gerade pausiert ist.
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"
}Mit agreement_status gleich paused funktioniert die Authentifizierung weiterhin, und jeder Lesezugriff wird weiter bedient. Das Pausieren ist eine Missbrauchskontrolle für die Registrierung, keine Sperre – behandle es nicht als Authentifizierungsfehler.
GET /partner/customers
Die Kunden, die dir derzeit zugeordnet sind.
{ "customers": [ { "tenant_id": "9c1e...", "org_slug": "acme" } ] }Die Marge zählt ab dem Zuordnungsdatum. Ein spät zugeordneter Kunde bringt für die Monate davor also nichts ein.
GET /partner/deals
Jedes Geschäft, das du registriert hast, das neueste zuerst.
{
"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 ist das Ende des 90-tägigen Fensters, das die Registrierung gesichert hat. Wir schicken dir 14 Tage und 3 Tage vor dem Ablauf eine E-Mail.
conflict_state gleich flagged bedeutet, dass ein anderer Partner dasselbe Unternehmen registriert hat. Solche Fälle entscheiden wir anhand nachgewiesener Vertriebsaktivität, nie danach, wer zuerst war, und wir nennen den anderen Partner nicht.
POST /partner/deals
Registriert einen Interessenten und startet das Schutzfenster. Erfordert 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/dealsPflichtfelder: end_customer_legal_name, end_customer_country (ISO 3166-1 alpha-2, Großbuchstaben).
{ "id": "7b40...", "flagged": false, "pinned_price_list_version": "2026-06" }| Status | Bedeutung |
|---|---|
201 | Registriert |
400 | Abgelehnt – der Body nennt die fehlgeschlagene Prüfung |
429 | Tägliches Registrierungslimit erreicht |
flagged: true ist ein 201, kein Fehler. Das Geschäft ist registriert; ein anderer Partner hat dasselbe Unternehmen registriert, und wir klären das. Versuche es nicht erneut – ein neuer Versuch verbraucht dein Tageskontingent und ändert nichts.Fülle sales_activity_evidence sorgfältig aus. Auf diesem Nachweis beruht die Entscheidung über einen Konflikt.
POST /partner/deals/{dealId}/enterprise-request
Fragt verhandelte Nutzerpreise an, bevor der Kunde angelegt wird. Erfordert 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 }Antwortet immer mit 200. recorded: false bedeutet, dass die Anfrage nicht übernommen wurde – bewusst ohne zu unterscheiden, ob schon angefragt wurde oder ob es nicht dein Geschäft ist.
Die erste Anfrage gilt: Eine zweite verschiebt weder das erfasste Datum noch ersetzt sie die Notiz. Angenommen wird die Anfrage nur, solange das Geschäft genehmigt oder gewonnen ist und noch kein Kunde existiert; danach ist der Mandant real, und ihn umzustellen ist Sache eines Operators.
POST /partner/deals/{dealId}/customer
Bringt den Kunden für ein gewonnenes Geschäft auf die Plattform. Erfordert 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.../customerPflichtfelder: org_slug (Kleinbuchstaben, DNS-tauglich, 3–30 Zeichen – daraus wird frem.sh/<slug>, und das lässt sich nicht ändern), display_name, admin_email.
Dabei wird die Organisation angelegt, der Administrator per E-Mail eingeladen und der Kunde dir zugeordnet, sodass die Marge ab seiner ersten Rechnung zählt.
Der Kunde startet mit einer Testphase. Der Tarif ist bewusst kein Parameter – ihn zu wählen hieße, kommerzielle Konditionen zu gewähren. Die Antwort enthält billing_setup_url; dort hinterlegt der Kunde eine Zahlungsmethode und wechselt in den bezahlten Tarif. Denselben Link bekommt er per E-Mail.
Abgelehnt, wenn das Geschäft nicht dir gehört, nicht genehmigt oder gewonnen ist, schon einen Kunden hat oder die Registrierung für dich pausiert ist. 429, wenn das tägliche Limit für das Anlegen von Kunden erreicht ist.
GET /partner/statements
Deine Margenabrechnungen, der neueste Zeitraum zuerst.
{
"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
}
]
}Abrechnungen werden am 1. für den vorangegangenen Kalendermonat erstellt.
Die Abwicklung folgt der Reihenfolge sent_at → partner_invoice_ref → paid_at. Bezahlt wird erst, wenn eine Rechnungsreferenz des Partners vorliegt, denn deine Rechnung ist das Dokument, auf das sich die Zahlung bezieht. Die Zahlung erfolgt 30 Tage nach ihrem Eingang.
Ein Ablauf als Beispiel
Aus einem CRM registrieren, sobald eine Verkaufschance angelegt wird, und dann den Kunden anlegen, wenn sie gewonnen ist:
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")"'Fehler
Partner-Endpunkte verwenden das plattformweite Format application/problem+json. Die type-URL führt zu einer Seite, die den konkreten Fehler beschreibt – siehe API-Fehler.
Die vollständige Spezifikation
Diese Endpunkte sind Teil des veröffentlichten OpenAPI-Dokuments, unter dem Tag Partner:
Die Spezifikation wird aus dem laufenden Dienst erzeugt und ist maßgeblich, wenn sie und diese Seite voneinander abweichen.