Gå til hovedindhold

Partner-API

Med Partner-API’et kan en partner køre hele det kommercielle forløb fra sine egne systemer: registrere et deal i det øjeblik, en salgsmulighed oprettes, oprette kunden på platformen, når dealet er vundet, og afstemme marginopgørelser mod sit eget regnskab.

Hvad forløbet betyder kommercielt – beskyttelsesvinduer, konflikter, hvordan margin beregnes – står i vejledningen til partnerprogrammet, som du bør læse først. Denne side handler om mekanikken.

Basis-URL:

https://frem.sh/_app/api/v1
Bemærk præfikset /_app. frem.sh/api/v1/... er Forgejos eget API, som er en anden overflade med andre tokens. Et partnerkald, der sendes dertil, fejler ikke på en tydelig måde – det bliver bare besvaret af Forgejo.

Godkendelse

Partner-endpoints tager et personligt fremforge-adgangstoken som bearer-token:

curl -H "Authorization: Bearer $FF_TOKEN" \
  https://frem.sh/_app/api/v1/partner/me

To scopes styrer adgangen:

ScopeGiver adgang til
partner:read/partner/me, /partner/customers, /partner/deals (GET), /partner/statements
partner:writeRegistrering af deals, anmodning om enterprisevilkår, oprettelse af kunder

En integration, der kun læser – et dashboard, et afstemningsjob – bør kun have partner:read.

Sådan får du et token

Udsted et i partnerportalen under Din adgang, eller fra et eksisterende token med scopet 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 er som standard 30 og højst 90, så en integration, der kører længe, skal have en plan for rotation i stedet for et token, den kan sætte op og glemme. Tokenværdien vises én gang. Se Administration af tokens om rotation og tilbagekaldelse.

To måder, et kald kan blive afvist på

De betyder forskellige ting, og det er værd at skelne mellem dem i din fejlhåndtering:

  • 403 not_a_partner – tokenet er gyldigt, men dets ejer er ikke en partnerbruger. Som regel det forkerte token.
  • 403 insufficient-scope – den rigtige identitet, men scopet mangler. Udsted et nyt med partner:write.

Ingen af stavemåderne er en tastefejl på denne side: not_a_partner bruger understregning, insufficient-scope en bindestreg. Match på den nøjagtige streng.

Partnerdata går bevidst på tværs af tenants: disse endpoints er ikke knyttet til en organisation, og en partners egen tenant er udelukket fra de tenants, partneren ellers kan administrere.

GET /partner/me

Hvem du handler som, og om dealregistrering lige nu er sat på pause for dig.

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"
}

agreement_status med værdien paused godkendes stadig og leverer stadig alle læsninger. En pause er en kontrol mod misbrug af registrering, ikke en udelukkelse – behandl den ikke som en fejl i godkendelsen.

GET /partner/customers

De kunder, der i øjeblikket er tildelt dig.

{ "customers": [ { "tenant_id": "9c1e...", "org_slug": "acme" } ] }

Margin tæller fra tildelingsdatoen, så en kunde, der tildeles sent, giver intet for månederne før.

GET /partner/deals

Alle deals, du har registreret, med de nyeste først.

{
  "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 er slutningen på det beskyttelsesvindue på 90 dage, som registreringen gav. Vi sender dig en e-mail 14 dage og 3 dage, før det udløber.

conflict_state med værdien flagged betyder, at en anden partner har registreret den samme virksomhed. Vi afgør den slags ud fra dokumentation for salgsaktivitet, aldrig ud fra, hvem der kom først, og vi oplyser ikke, hvem den anden partner er.

POST /partner/deals

Registrér en mulig kunde, og start beskyttelsesvinduet. Kræver 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

Påkrævet: end_customer_legal_name, end_customer_country (ISO 3166-1 alpha-2, store bogstaver).

{ "id": "7b40...", "flagged": false, "pinned_price_list_version": "2026-06" }
StatusBetydning
201Registreret
400Afvist – indholdet angiver, hvilket tjek der fejlede
429Det daglige loft over registreringer er nået
flagged: true er en 201, ikke en fejl. Dealet er registreret; en anden partner har registreret den samme virksomhed, og vi afgør det. Prøv ikke igen – et nyt forsøg bruger af din daglige kvote og ændrer intet.

Udfyld sales_activity_evidence ordentligt. Det er den dokumentation, en konflikt afgøres ud fra.

POST /partner/deals/{dealId}/enterprise-request

Bed om forhandlede priser pr. bruger, før du opretter kunden. Kræver 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 }

Svarer altid 200. recorded: false betyder, at anmodningen ikke blev registreret – bevidst uden at skelne mellem “allerede anmodet” og “ikke dit deal”.

Den første skrivning gælder: at spørge to gange flytter ikke den registrerede dato og erstatter ikke noten. Accepteres kun, mens dealet er godkendt eller vundet, og der endnu ikke findes en kunde; derefter er tenanten virkelig, og at konvertere den er en opgave for operatøren.

POST /partner/deals/{dealId}/customer

Opret kunden på platformen for et deal, du har vundet. Kræver 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

Påkrævet: org_slug (små bogstaver, DNS-sikker, 3–30 tegn – den bliver til frem.sh/<slug> og kan ikke ændres), display_name, admin_email.

Det opretter organisationen, inviterer administratoren via e-mail og tildeler kunden til dig, så margin tæller fra kundens første faktura.

Kunden starter på en prøveperiode. Abonnementet er bevidst ikke en parameter – at vælge det ville være at give kommercielle vilkår. Svaret indeholder billing_setup_url, hvor kunden tilføjer en betalingsmetode og konverterer; det samme link sendes til kunden via e-mail.

Afvises, når dealet ikke er dit, ikke er godkendt eller vundet, allerede har en kunde, eller når registrering er sat på pause for dig. 429, når det daglige loft over oprettelse af kunder er nået.

GET /partner/statements

Dine marginopgørelser med den nyeste periode først.

{
  "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
    }
  ]
}

Opgørelser dannes den 1. i måneden for den foregående kalendermåned.

Beløbsfelterne er decimalstrenge, der returneres præcis, som de er gemt. Parse dem ikke som flydende tal, og afrund dem ikke igen, ellers vil din afstemning ikke stemme med den opgørelse, den kom fra.

Afregning sker i rækkefølgen sent_at → partner_invoice_ref → paid_at. Intet udbetales, før der findes en reference til en partnerfaktura, fordi din faktura er det bilag, betalingen sker imod. Betaling sker 30 dage efter, at vi har modtaget den.

Et gennemgået forløb

Registrering fra et CRM i det øjeblik, en salgsmulighed oprettes, og oprettelse af kunden, når den er vundet:

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")"'

Fejl

Partner-endpoints bruger platformens fælles application/problem+json-form. type-URL’en fører til en side, der beskriver den konkrete fejl – se API-fejl.

Den fulde specifikation

Disse endpoints er en del af det offentliggjorte OpenAPI-dokument under tagget Partner:

Specifikationen genereres ud fra den kørende tjeneste og er den gældende, hvor den og denne side er uenige.