Gå til hovedindhold
CustomerFlow CustomerFlow

API-dokumentation

REST API til at integrere CustomerFlow med dit CRM, Zapier, n8n eller egne værktøjer.

v1 . stable Bearer token + HMAC-signerede webhooks https://customerflow.dk/api/v1

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.

shell
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:

json
{
  "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 & OAuth.

Scopes & 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

Scope Giver adgang til Endpoints
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:

shell
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):

json
{
  "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.

json
{
  "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.

Tier Limit Anvendes på
api-read 120 req/min Alle GET-endpoints
api-write 60 req/min POST / 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 & /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 :

json
{
  "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.

shell
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 .

json
{
  "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 .

json
{
  "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:

shell
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):

json
{
  "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.

shell
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 ):

json
{
  "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

Event Fyrer 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

json
{
  "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)

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

disc.analysis.created

json
{
  "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

json
{
  "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

json
{
  "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:

http
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

php
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

javascript
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 & 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 & 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 .

json
{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "details": {
      "email": ["must be a valid email"]
    }
  }
}
Code Status Betydning
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 & 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

API'et er på v1 . Tilføjelse af nye endpoints, felter eller event-typer betragtes som bagudkompatible og varsles ikke specifikt. Din integration skal kunne ignorere ukendte felter.

Breaking changes (fjernelse eller omdøbning af felter, ændring af betydning) annonceres minimum 6 måneder før de gennemføres, via:

  • Direkte email til alle aktive API-nøgleejere.
  • Banner i administrationen.
  • En ny /v2/ -prefix der lever parallelt i en overgangsperiode.

Klar til at integrere?

Opret en API-nøgle i Indstillinger og kom i gang på få minutter.

Opret API-nøgle

Stærkere relationer med hver kunde

Book en demo, så viser vi hvordan CustomerFlow løfter jeres kundedialog på mail og telefon, tilpasset jeres salgsteam.

Vi vender tilbage inden for 24 timer