Zum Hauptinhalt springen

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
Achte auf das Präfix /_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/me

Zwei Scopes steuern den Zugriff:

ScopeErlaubt
partner:read/partner/me, /partner/customers, /partner/deals (GET), /partner/statements
partner:writeGeschä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 mit partner: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/deals

Pflichtfelder: end_customer_legal_name, end_customer_country (ISO 3166-1 alpha-2, Großbuchstaben).

{ "id": "7b40...", "flagged": false, "pinned_price_list_version": "2026-06" }
StatusBedeutung
201Registriert
400Abgelehnt – der Body nennt die fehlgeschlagene Prüfung
429Tä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.../customer

Pflichtfelder: 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 Geldfelder sind Dezimalzeichenketten und werden genau so zurückgegeben, wie sie gespeichert sind. Lies sie nicht als Gleitkommazahlen ein und runde sie nicht neu, sonst weicht dein Abgleich von der Abrechnung ab, aus der er stammt.

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.