Gå til hovedindhold

API-dokumentation

REST API til at integrere CustomerFlow med jeres CRM, Zapier, n8n eller egne værktøjer. JSON ind og ud, bearer token i headeren og webhooks signeret med HMAC-SHA256.

  • v1 · stable
  • Bearer token + HMAC-signerede webhooks
  • https://customerflow.dk/api/v1
På denne side

Kom i gang

CustomerFlow tilbyder et REST API hvor du kan læse kontakter, DISC-analyser, follow-ups og samtaleindsigt, samt sende kommunikation til ad-hoc AI-analyse uden at gemme data. API'et bruger Bearer-tokens og returnerer altid JSON.

  1. Log ind og gå til Indstillinger · API.
  2. Klik på Opret nøgle, giv den et beskrivende navn (fx "CRM-sync" ) og kopiér nøglen. Den vises kun én gang.
  3. Send nøglen som Authorization: Bearer <nøgle> -header.
  4. Base-URL er https://customerflow.dk/api/v1 .

API-adgang er et tilvalg, vi aktiverer på din konto. Teammedlemmer skal bede deres teamejer om at etablere integrationer.

Autentificering

Alle endpoints kræver en gyldig API-nøgle. API-adgang er et tilvalg, vi aktiverer på din konto - kontakt salg for at få det slået til.

ShellGET /v1/me
curl https://customerflow.dk/api/v1/me \
  -H "Authorization: Bearer cf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"

Et succesfuldt kald returnerer en JSON-envelope med data -feltet:

JSON200 OK
{
  "data": {
    "id": 42,
    "name": "Anders Andersen",
    "email": "anders@example.com",
    "role": "customer",
    "team_id": null,
    "is_team_owner": false,
    "scopes": null
  }
}

role er customer , admin eller super_admin . team_id er null for solo-brugere. scopes er listen af scopes tokenet er udstedt med (fx ["contacts:read", "disc:read"] ), eller null for en selvbetjenings-nøgle med fuld adgang. Se Scopes og OAuth.

Scopes og OAuth

Der findes to slags tokens. En selvbetjenings-nøgle (cf_live_... , oprettet under Indstillinger) har fuld adgang og er ikke begrænset af scopes. Et OAuth-token, som en tredjepartsintegration får via "Log ind med CustomerFlow", er begrænset til netop de scopes brugeren har godkendt. Begge sendes som Authorization: Bearer <token> .

Hvert endpoint kræver et bestemt scope. Mangler tokenet et krævet scope, returneres 403 insufficient_scope med de manglende scopes i details.required_scopes . GET /v1/me og GET /v1/quota kræver intet scope og kan altid kaldes med et gyldigt token. Du kan se et tokens scopes i scopes -feltet på /me .

Scope-katalog

ScopeGiver adgang tilEndpoints
contacts:read Læse kontakterGET /contacts , GET /contacts/{id}
contacts:write Oprette, opdatere, slette, gendanne og flette kontakterPOST /PATCH /DELETE /contacts + /restore , /merge
disc:read Læse DISC-analyser og kommunikationsrådGET /disc-analyses
insights:read Læse samtaleindsigtGET /conversation-insights
messages:read Læse emails og opkaldGET /email-messages , GET /phone-calls
follow-ups:read Læse follow-upsGET /follow-ups
follow-ups:write Oprette og opdatere follow-upsPOST /follow-ups + /complete , /dismiss
webhooks:read Læse webhook-opsætning og leveringslogGET /webhooks + /deliveries
webhooks:write Administrere webhooksPOST /PATCH /DELETE /webhooks + /test , /rotate-secret
analyze Køre ad-hoc AI-analyserPOST /analyze/*
sync Umbrella til tovejs-synk. Udvides ved udstedelse til contacts:read , contacts:write , disc:read og messages:read Se de udvidede scopes

Beder en klient om sync , gemmes de fire udvidede scopes på tokenet, og scopes/me viser dem enkeltvis. En selvbetjenings-nøgle har scopes: null og springer alle scope-tjek over.

Log ind med CustomerFlow (OAuth2)

En tredjepartsintegration kan bede en CustomerFlow-bruger om adgang via OAuth2 authorization-code med PKCE. Flowet er beregnet til udvidelser og connectors der skal handle på brugerens vegne uden at brugeren deler sin nøgle. OAuth-klienter oprettes af CustomerFlow - kontakt os for at få et client_id og en godkendt redirect_uri .

  1. Send brugeren til GET https://customerflow.dk/oauth/authorize med query-parametrene nedenfor. Er brugeren ikke logget ind, sendes de først gennem CustomerFlows login (inkl. Microsoft SSO) og tilbage igen.
  2. Brugeren ser en samtykke-skærm med de scopes I beder om, og godkender eller afviser.
  3. Ved godkendelse omdirigeres brugeren til jeres redirect_uri med ?code=...&state=... . Ved afvisning følger ?error=access_denied (og error_code=api_access_required hvis kontoen ikke har API-adgang).
  4. Byt koden til et token på POST https://customerflow.dk/api/oauth/token (se nedenfor). Koden er kortlivet og kan kun bruges én gang.

Query-parametre til /oauth/authorize :

ParameterNote
response_type Skal være code .
client_id Jeres klient-ID.
redirect_uri Skal matche en godkendt URI præcist (open-redirect-værn).
scope Mellemrums-separeret liste af scopes fra kataloget ovenfor.
state Valgfri, men anbefalet (CSRF-værn, max 512 tegn). Returneres uændret.
code_challenge PKCE-challenge.
code_challenge_method S256 (standard) eller plain .

Byt koden til et token:

ShellPOST /api/oauth/token
curl -X POST https://customerflow.dk/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "din-klient",
    "code": "<code fra redirect>",
    "redirect_uri": "https://din-app.dk/callback",
    "code_verifier": "<PKCE verifier>"
  }'

Respons (200 OK ). expires_in er sekunder (standard 3600 = 1 time):

JSON200 OK
{
  "access_token": "cf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "cf_refresh_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "scope": "contacts:read contacts:write disc:read messages:read"
}
  • Fortrolige klienter: send desuden client_secret i body. Offentlige klienter (rene PKCE) sender intet secret.
  • Forny: POST /api/oauth/token med grant_type: refresh_token , client_id og refresh_token . Refresh-tokens roteres ved hver fornyelse (genbrug af et gammelt token tilbagekalder forbindelsen). Refresh-token gælder 30 dage.
  • Tilbagekald: POST /api/oauth/revoke med { "token": "..." } (access- eller refresh-token). Returnerer altid { "revoked": true } og lækker ikke om tokenet fandtes.
  • Token-endpointet svarer med error.code ved fejl, fx unsupported_grant_type (400), invalid_client eller invalid_grant .

Konventioner

HTTP-statuskoder

  • 200 OK: succesfuldt GET, PATCH eller action (fx complete/dismiss/test).
  • 201 Created: succesfuld POST der opretter en ressource.
  • 204 No Content: succesfuld DELETE. Body er tom.
  • 4xx / 5xx: returnerer altid en fejl-envelope. Se Fejlkoder.

Pagination

Alle listings er pagineret. Standardværdier: per_page=25 , maks per_page=100 . Brug ?page=N til at navigere.

JSONGET /v1/contacts
{
  "data": [
    { "id": 309, "name": "Anders Andersen", "email": "anders@example.com", ... },
    { "id": 312, "name": "Brian Brun", "email": "brian@example.com", ... }
  ],
  "links": {
    "first": "https://customerflow.dk/api/v1/contacts?page=1",
    "last": "https://customerflow.dk/api/v1/contacts?page=6",
    "prev": null,
    "next": "https://customerflow.dk/api/v1/contacts?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 6,
    "per_page": 25,
    "to": 25,
    "total": 137
  }
}

Filter-syntax

  • Booleans: brug 1 /0 eller true /false (fx ?is_blocked=1 ).
  • Datoer: ISO 8601, fx ?updated_since=2026-05-01T00:00:00Z .
  • Enums: brug værdien præcis som dokumenteret (fx ?primary_type=D , ?status=pending ). Ukendte værdier returnerer 422 validation_failed med en liste over tilladte værdier.
  • Fritekst (q på kontakter): matcher mod name , email , company , phone .

Tidsstempler og tegnsæt

Alle datoer og tidsstempler returneres som ISO 8601 i UTC (fx 2026-05-21T08:30:00+00:00 ). Strenge er UTF-8.

Rate limits

Rate limits er pr. nøgle (ikke pr. bruger), så flere nøgler får hver deres bucket. Hver respons returnerer headerne X-RateLimit-Limit og X-RateLimit-Remaining . Ved 429 returneres også Retry-After i sekunder.

TierLimitAnvendes på
api-read 120 req/minAlle GET-endpoints
api-write 60 req/minPOST / PATCH / DELETE
api-analyze 15 req/min/analyze/* (AI-kald)

Ud over rate limits gælder en månedlig fair-use-grænse for /analyze/* -endpoints. Grænsen er fælles for hele teamet og skalerer med antal seats (1000 analyser pr. seat pr. måned). Hvis I rammer den, får I 429 fair_use_exceeded med Retry-After sat til sekunder indtil måneds-rollover. Du kan følge forbruget via GET /v1/quota .

Endpoints

Alle endpoints lever under https://customerflow.dk/api/v1/ . Hver ressource er scoped til den nøgle der kalder. Du ser aldrig andres data, og direkte opslag af en anden brugers ID returnerer 404 not_found .

/me og /quota

  • GET /v1/me : den autentificerede bruger og team-tilhørsforhold.
  • GET /v1/quota : aktuelt månedligt forbrug af analyse- og voice-quota.

Eksempel-response for GET /v1/quota :

JSONGET /v1/quota
{
  "data": {
    "analysis": {
      "used": 47,
      "limit": 999999,
      "remaining": 999952,
      "period_start": "2026-05-01",
      "period_end": "2026-05-31"
    },
    "voice": {
      "used": 312,
      "limit": 999999,
      "remaining": 999687,
      "period_start": "2026-05-01",
      "period_end": "2026-05-31"
    },
    "api_fair_use": {
      "used": 1240,
      "limit": 5000,
      "remaining": 3760,
      "period_start": "2026-05-01",
      "period_end": "2026-05-31"
    }
  }
}

analysis tæller email-, transskript- og samtaleanalyser i appen. voice tæller telefon-minutter. api_fair_use er den månedlige fair-use-grænse for API-analyser (fælles pulje pr. team, 1000 pr. seat). Felter er null hvis brugeren ikke har en aktiv periode (fx admin-konto uden loft).

Kontakter

CRUD + merge. DISC-scores er AI-styret og kan ikke skrives via API'et.

  • GET /v1/contacts : filtrér med q , primary_type , tag , is_blocked , do_not_call , updated_since .
  • GET /v1/contacts/{id}
  • POST /v1/contacts201 Created
  • PATCH /v1/contacts/{id}
  • DELETE /v1/contacts/{id}204 No Content . Soft delete: sætter deleted_at ; relaterede records bevares.
  • POST /v1/contacts/{id}/restore
  • POST /v1/contacts/{id}/merge , body: { "target_contact_id": 123 }

Skrivbare felter på POST og PATCH . Mindst ét af name , email eller phone skal være satPOST .

FeltTypeNote
name string (1-255)Valgfrit.
email stringValgfrit. Skal være valid email.
phone string (E.164)Normaliseres automatisk (fx +4520202020 ).
company stringValgfrit.
cvr_number string (max 8)Dansk CVR-nummer.
notes stringFritekst.
tags string[]Fx ["vip", "inbound"] .
is_blocked booleanBlock-flag.
do_not_call booleanMarkerér som "ring ikke".
recording_enabled booleanPer-kontakt override af opkalds-optagelse.
assigned_user_id integerKun ved POST + kun teamejere. Skal være medlem af eget team, ellers 422 .

Eksempel: opret en kontakt.

ShellPOST /v1/contacts
curl -X POST https://customerflow.dk/api/v1/contacts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Anders Andersen",
    "email": "anders@example.com",
    "phone": "+4520202020",
    "company": "Test ApS",
    "tags": ["vip", "inbound"]
  }'

Respons: 201 Created .

JSON201 Created
{
  "data": {
    "id": 412,
    "user_id": 7,
    "team_id": 3,
    "name": "Anders Andersen",
    "email": "anders@example.com",
    "phone": "+4520202020",
    "company": "Test ApS",
    "cvr_number": null,
    "notes": null,
    "tags": ["vip", "inbound"],
    "is_blocked": false,
    "do_not_call": false,
    "recording_enabled": null,
    "disc": {
      "d_score": null,
      "i_score": null,
      "s_score": null,
      "c_score": null,
      "primary_type": null
    },
    "style_match": null,
    "ai_summary": null,
    "ai_summary_generated_at": null,
    "analysis_count": 0,
    "created_at": "2026-05-21T08:30:00+00:00",
    "updated_at": "2026-05-21T08:30:00+00:00",
    "deleted_at": null
  }
}

style_match sammenholder token-brugerens egen kommunikationsprofil med kontaktens og er null , indtil begge profiler findes. Ellers indeholder feltet level (natural |light |conscious ), level_label (dansk), seller_type , customer_type og advice (liste med konkrete råd).

DISC-analyser

Read-only. Hver analyse er knyttet til den email eller det opkald der genererede den, og eventuelt en kontakt.

  • GET /v1/disc-analyses : filtrér med contact_id , primary_type (D|I|S|C), from , to .
  • GET /v1/disc-analyses/{id} : inkluderer dekrypterede reply_suggestions , communication_tips og rationale .

Hver analyse indeholder også salgssignaler udtrukket fra materialet: buying_signals , objections og competitor_mentions (lister med korte danske udsagn, tomme hvis ingen) samt urgency (low |medium |high eller null ). Analyser lavet før signalfelterne blev introduceret returnerer tomme lister og null .

Ud over scores og forslag returnerer hver analyse metadata om selve AI-kørslen: llm_model (modellen der lavede analysen), tokens_used (int), refined_at (tidsstempel eller null , sat hvis analysen er blevet raffineret) og refinement_count (int). Ressourcer kan indeholde flere additive felter over tid - ignorér ukendte felter.

Follow-ups

En follow-up kan være knyttet til en kontakt eller stå alene som en påmindelse eller opgave. contact_id er derfor valgfrit.

  • GET /v1/follow-ups : filtrér med status (pending |completed |dismissed ), contact_id .
  • GET /v1/follow-ups/{id}
  • POST /v1/follow-ups201 . Skrivbare felter nedenfor.
  • POST /v1/follow-ups/{id}/complete
  • POST /v1/follow-ups/{id}/dismiss

Skrivbare felter på POST . Kun suggested_date er påkrævet. user_id , status , source og completed_at er server-styrede og ignoreres hvis de sendes med.

FeltTypeNote
contact_id integerValgfrit. Sat, skal kontakten være synlig for dig (ellers 404 ). Udeladt bliver follow-up'en kontaktløs.
type stringValgfrit. reminder eller task . Standard er task .
title string (max 255)Valgfrit. Kort overskrift.
suggested_date datePåkrævet. I dag eller senere (YYYY-MM-DD ).
due_at datetimeValgfrit. Nu eller senere. Præcist forfaldstidspunkt (ISO 8601).
suggested_message string (max 5000)Valgfrit. Fritekst.

Respons: 201 Created .

JSON201 Created
{
  "data": {
    "id": 88,
    "user_id": 7,
    "contact_id": 412,
    "email_message_id": null,
    "phone_call_id": null,
    "meeting_id": null,
    "status": "pending",
    "source": "user_created",
    "type": "task",
    "title": "Ring og bekræft levering",
    "suggested_date": "2026-07-25",
    "due_at": "2026-07-25T09:00:00+00:00",
    "suggested_message": "Kort opkald for at bekræfte leveringsdato.",
    "completed_at": null,
    "created_at": "2026-07-22T08:30:00+00:00",
    "updated_at": "2026-07-22T08:30:00+00:00",
    "meeting": null
  }
}

Læse-felter: source er user_created eller ai_suggested . type kan ved læsning også være AI-genererede værdier - sales_follow_up og call_action_item - ud over reminder og task . Via API kan du kun sætte reminder eller task . meeting_id peger på et evt. tilknyttet møde, og meeting kan indeholde det indlejrede møde-objekt (ellers null ).

Conversation insights

Tråd-niveau analyse af email-samtaler: engagement-niveau, sentiment-trend, anbefalet handling.

  • GET /v1/conversation-insights : filtrér med contact_id , conversation_id .
  • GET /v1/conversation-insights/{id}

Emails

  • GET /v1/email-messages : filtrér med contact_id , from , to (ISO 8601 dato), is_analyzed .
  • GET /v1/email-messages/{id}

Opkald

Læs opkald inklusiv dekrypteret transskript og taler-diariserede segmenter.

  • GET /v1/phone-calls : filtrér med contact_id , direction (outbound |inbound ), status , from , to .
  • GET /v1/phone-calls/{id} : inkluderer segments[] med taler-label, tekst og start/slut i millisekunder.

Transskriberede opkald indeholder samtale-metrik beregnet fra segmenterne: talk_ratio (sælgerens andel af taletiden i procent, 0-100), question_count (antal spørgsmål sælgeren stillede) og longest_monologue_seconds (længste sammenhængende sælger-monolog). Alle tre er null , indtil transskriptionen er behandlet. is_voicemail (bool) markerer optagelser uden reel kundedialog.

Analyse (ephemeral)

POST tekst til AI-analyse uden persistens. Hver succesfuld request tæller som ét forbrug af teamets månedlige fair-use-grænse (1000 pr. seat). Ved upstream-timeout (504) eller -fejl (502) forbruges intet.

  • POST /v1/analyze/email , body: { "text": "..." } (min 20, max 50.000 tegn) → DISC-scores + reply_suggestions.
  • POST /v1/analyze/transcript , body: { "text": "..." } (min 20, max 200.000 tegn) → DISC fra samtale.
  • POST /v1/analyze/conversation , body: { "emails": [...] } (min 2, max 50 emails, hver body min 10 og max 50.000 tegn, total max 200 KB) → engagement, sentiment, summary.
  • POST /v1/analyze/follow-up-suggestion , body: { "disc_type": "D|I|S|C", "context": "..." } (context min 20, max 5.000 tegn) → besked + dato-forslag.

Eksempel:

ShellPOST /v1/analyze/email
curl -X POST https://customerflow.dk/api/v1/analyze/email \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text":"Vi skal have lukket dealen denne uge. Send tilbud nu."}'

Respons (200 OK):

JSON200 OK
{
  "data": {
    "d_score": 65,
    "i_score": 15,
    "s_score": 10,
    "c_score": 10,
    "primary_type": "D",
    "confidence_score": 88,
    "rationale": "Direkte handlingsorienteret formulering...",
    "communication_tips": ["Vær kort", "Lever bottom line først"],
    "reply_suggestions": {
      "short": "Tilbud er på vej i dag.",
      "neutral": "Tak. Jeg sender et tilbud i løbet af dagen.",
      "detailed": "Tak for opfølgningen. Tilbuddet er klar..."
    },
    "buying_signals": ["Beder om tilbud nu"],
    "objections": [],
    "competitor_mentions": [],
    "urgency": "high"
  }
}

Webhooks

Tilmeld dig events fra CustomerFlow så du modtager nye kontakter, DISC-analyser og opkald i dit system i realtid. URL skal være HTTPS. Private og loopback IP-adresser afvises.

Opret og administrér

Webhooks kan oprettes via UI'et (Indstillinger · API · Webhooks) eller via API'et:

  • GET /v1/webhooks : listing.
  • POST /v1/webhooks201 . Body: name (max 80), url (HTTPS, max 2048), events (array, mindst én). Returnerer signing_secret i plaintext én gang.
  • GET /v1/webhooks/{id} : detail. Indeholder ikke signing_secret.
  • PATCH /v1/webhooks/{id} : opdatér name , url , events eller is_active .
  • DELETE /v1/webhooks/{id}204 .
  • POST /v1/webhooks/{id}/test : sender et ping -event. Test-leveringer tæller ikke mod auto-disable.
  • POST /v1/webhooks/{id}/rotate-secret : genererer ny signing_secret og invaliderer den gamle øjeblikkeligt. Responsen har samme shape som POST og inkluderer det nye secret én gang.
  • GET /v1/webhooks/{id}/deliveries : paginated leveringslog (status, response-kode, retry-tidspunkter).

Eksempel: opret en webhook der lytter på flere events.

ShellPOST /v1/webhooks
curl -X POST https://customerflow.dk/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mit CRM",
    "url": "https://din-server.dk/webhooks/customerflow",
    "events": ["contact.created", "contact.updated", "disc.analysis.created"]
  }'

Respons (201 Created ):

JSON201 Created
{
  "data": {
    "id": 18,
    "user_id": 7,
    "team_id": 3,
    "name": "Mit CRM",
    "url": "https://din-server.dk/webhooks/customerflow",
    "events": ["contact.created", "contact.updated", "disc.analysis.created"],
    "is_active": true,
    "consecutive_failed_deliveries": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "auto_disabled_at": null,
    "auto_disabled_reason": null,
    "created_at": "2026-05-21T08:30:00+00:00",
    "signing_secret": "a1b2c3d4e5f6...64-hex"
  }
}

Gem signing_secret sikkert. Det vises kun denne ene gang. Mister du det, kør POST /v1/webhooks/{id}/rotate-secret for et nyt.

Event-typer

EventFyrer når
contact.created Ny kontakt oprettet.
contact.updated Kontakt opdateret (inkl. restore, hvor deleted_at = null ).
contact.deleted Kontakt soft-deletes. Payload trimmes til {id, deleted_at} . Ingen PII.
disc.analysis.created AI har gennemført DISC-analyse.
email.analyzed Email markeret som analyseret (is_analyzed: false → true ).
call.completed Opkald afsluttet og analyseret. Inkluderer transskript (trunkeres ved > 800 KB).
conversation.insight.created Tråd-niveau indsigt genereret første gang.
conversation.insight.updated Eksisterende tråd-indsigt re-analyseret (samme payload-shape som .created ).
follow_up.created Follow-up oprettet.
follow_up.completed Follow-up markeret som completed.

Privacy-noter: forceDelete() (GDPR-purge) fyrer ALDRIG contact.deleted . Slettet kontakt slettes tavst. Restore-flow fyrer præcis ét contact.updated , ikke to.

Payload-eksempler

Hver POST har Content-Type: application/json . Body følger de eksempler vist nedenfor. Ukendte felter kan dukke op i fremtidige versioner. Ignorér dem.

contact.created · contact.updated

JSONcontact.created
{
  "id": 412,
  "user_id": 7,
  "team_id": 3,
  "name": "Anders Andersen",
  "email": "anders@example.com",
  "phone": "+4520202020",
  "company": "Test ApS",
  "tags": ["vip"],
  "is_blocked": false,
  "do_not_call": false,
  "disc": {
    "d_score": 40,
    "i_score": 30,
    "s_score": 20,
    "c_score": 10,
    "primary_type": "D"
  },
  "deleted_at": null,
  "created_at": "2026-05-21T08:30:00+00:00",
  "updated_at": "2026-05-21T08:30:00+00:00"
}

contact.deleted (PII-trimmet)

JSONcontact.deleted
{
  "id": 412,
  "deleted_at": "2026-05-21T09:45:00+00:00"
}

disc.analysis.created

JSONdisc.analysis.created
{
  "id": 5821,
  "user_id": 7,
  "contact_id": 412,
  "email_message_id": 11023,
  "phone_call_id": null,
  "d_score": 40,
  "i_score": 30,
  "s_score": 20,
  "c_score": 10,
  "primary_type": "D",
  "confidence_score": 85,
  "rationale": "Direkte, handlingsorienteret sprog...",
  "communication_tips": ["Vær kort", "Lever bottom line først"],
  "reply_suggestions": {
    "short": "Tak. Send tilbud i dag.",
    "neutral": "Tak. Jeg sender et tilbud i løbet af dagen.",
    "detailed": "Tak for opfølgningen. Tilbuddet er klar..."
  },
  "buying_signals": ["Beder om tilbud i dag"],
  "objections": [],
  "competitor_mentions": [],
  "urgency": "high",
  "created_at": "2026-05-21T08:30:00+00:00"
}

call.completed

JSONcall.completed
{
  "id": 9011,
  "user_id": 7,
  "contact_id": 412,
  "direction": "outbound",
  "from_number": "+4570201020",
  "to_number": "+4520202020",
  "status": "completed",
  "duration_seconds": 187,
  "started_at": "2026-05-21T08:25:00+00:00",
  "ended_at":   "2026-05-21T08:28:07+00:00",
  "transcript": "A: Hej Anders, har du tid til en kort snak?\nB: Ja, hvad drejer det sig om?\n...",
  "transcript_truncated": false,
  "is_analyzed": true,
  "is_voicemail": false,
  "talk_ratio": 46,
  "question_count": 8,
  "longest_monologue_seconds": 34
}

follow_up.created · follow_up.completed

JSONfollow_up.created
{
  "id": 88,
  "user_id": 7,
  "contact_id": 412,
  "status": "pending",
  "source": "user_created",
  "type": "task",
  "title": "Ring og bekræft levering",
  "suggested_date": "2026-07-25",
  "due_at": "2026-07-25T09:00:00+00:00",
  "suggested_message": "Kort opkald for at bekræfte leveringsdato.",
  "completed_at": null,
  "created_at": "2026-07-22T08:30:00+00:00"
}

follow_up.completed har samme shape med status: "completed" og et udfyldt completed_at . contact_id er null for kontaktløse follow-ups.

Andre events (email.analyzed , conversation.insight.created ) følger samme principper. Webhook-payloads er en kurateret undermængde af det tilsvarende GET -endpoint - fx udelades segments , recording_consent og voice_minutes_charged fra call.completed , og meeting fra follow_up.* . Ukendte felter kan dukke op i fremtidige versioner - ignorér dem.

Verificér signatur

Hver POST kommer med følgende headers:

HTTPRequest headers
X-CustomerFlow-Signature: t=1716200000,v1=4d8b3a...
X-CustomerFlow-Event: contact.created
X-CustomerFlow-Delivery: 0fcc77b8-2f6a-4f25-9c39-9ea1c2bc7e15
Content-Type: application/json

Genberegn HMAC-SHA256 over "<t>.<raw-body>" med din signing-secret og sammenlign konstanttidsmæssigt. Afvis hvis tidsstempel er mere end 5 minutter gammelt. Det er replay-vinduet.

PHP

PHPverify()
function verify(string $body, string $header, string $secret): bool {
    if (! preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $m)) {
        return false;
    }
    [, $t, $v1] = $m;
    if (abs(time() - (int)$t) > 300) return false;
    $expected = hash_hmac('sha256', $t . '.' . $body, $secret);
    return hash_equals($expected, $v1);
}

Node.js

JavaScriptverify()
import crypto from 'node:crypto';

function verify(body, header, secret) {
  const match = header.match(/t=(\d+),v1=([a-f0-9]+)/);
  if (!match) return false;
  const [, t, v1] = match;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${body}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(v1, 'hex'),
  );
}

Levering og idempotens

  • Asynkron: leveringer sendes via en baggrundskø, så der kan gå et øjeblik fra eventet sker til din endpoint kaldes.
  • At-least-once: pga. retries kan du modtage samme levering mere end én gang. Dedupér på X-CustomerFlow-Delivery (unik UUID pr. levering) og spring leveringer over du allerede har behandlet. Gør din handler idempotent.
  • Svar hurtigt: vi venter højst 10 sekunder på et svar. Svar 2xx med det samme og processér tungt arbejde bagefter - ellers tæller det som timeout og udløser en retry (og dermed en dublet).
  • Ingen rækkefølge-garanti: events leveres ikke nødvendigvis i den rækkefølge de skete. Brug tidsstemplerne i payloaden hvis rækkefølge er vigtig.
  • Redirects følges ikke: peg url direkte på din endelige endpoint. Et 3xx -svar behandles som en mislykket levering.

Retry og auto-disable

  • 2xx: leveringen markeres som success .
  • 5xx, 408, 429 eller netværksfejl: retry med backoff [1m, 5m, 30m, 2h, 12h] . Efter 5 mislykkede forsøg markeres leveringen dead .
  • Andre 4xx (400, 401, 403, 404, 422): failed uden retry. Det er en consumer-fejl, og retry hjælper ikke.
  • Auto-disable: hvis en webhook er ældre end 48 timer, har 25+ fejlede leveringer i træk OG ingen succesfuld levering de seneste 24 timer, deaktiveres den automatisk. Test-leveringer påvirker ikke tælleren.

Fejlkoder

Alle fejl returneres som JSON-envelope med error.code , error.message og eventuelt error.details .

JSON422 Unprocessable Content
{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "details": {
      "email": ["must be a valid email"]
    }
  }
}
CodeStatusBetydning
bad_request 400Forespørgslen er ugyldig eller mangler påkrævede parametre.
invalid_token 401Bearer mangler, er ugyldig, eller tilbagekaldt.
subscription_inactive 403Nøglens ejer har intet aktivt abonnement.
api_access_required 403API-adgang er ikke aktiveret på kontoen. Kontakt salg for at få det slået til.
forbidden 403Auth ok, men handlingen er ikke tilladt.
insufficient_scope 403Tokenet mangler et krævet scope. De manglende scopes står i details.required_scopes . Se Scopes og OAuth.
not_found 404Ressource findes ikke (eller du har ikke adgang).
method_not_allowed 405HTTP-metoden er ikke tilladt på dette endpoint.
conflict 409Fx merge mellem inkompatible kontakter.
validation_failed 422Body fejler validation. Se details .
rate_limited 429For mange requests. Se Retry-After .
fair_use_exceeded 429Månedlig API fair-use-grænse nået (fælles pr. team, 1000 pr. seat). Retry-After = sekunder til måneds-rollover.
server_error 500Uventet fejl. Prøv igen senere, ellers kontakt support.
upstream_unavailable 502Anthropic returnerede fejl. Intet forbrugt.
upstream_timeout 504Anthropic-timeout (>30s). Intet forbrugt.

Information-leak-regel: Vi returnerer 404 i stedet for 403 , når du forsøger at tilgå en anden brugers eller anden teams ressource. På den måde lækker API'et aldrig eksistens af ressourcer du ikke har adgang til.

Versionering

Versionen ligger i stien. API'et er på v1 . Breaking changes (fjernelse eller omdøbning af felter, ændring af betydning) udkommer som en ny version med eget prefix, fx /v2/ , og den forrige version holdes kørende i mindst 12 måneder efter, så I har tid til at flytte. Vi varsler via:

  • Direkte email til alle aktive API-nøgleejere.
  • Banner i administrationen.

Tilføjelse af nye endpoints, felter eller event-typer betragtes som bagudkompatible og varsles ikke specifikt. Din integration skal kunne ignorere ukendte felter.

Skal vi hjælpe med integrationen?

Vi kigger gerne med på jeres opsætning, inden I bygger.

Kontakt salg

Har I allerede API-adgang? Opret en nøgle under Indstillinger · API