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.
- Log ind og gå til Indstillinger . API.
- Klik på Opret nøgle, giv den et beskrivende navn (fx
"CRM-sync") og kopiér nøglen. Den vises kun én gang. - Send nøglen som
Authorization: Bearer <nøgle>-header. - 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.
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:
{
"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 kontakter | GET /contacts
, GET /contacts/{id}
|
contacts:write
| Oprette, opdatere, slette, gendanne og flette kontakter | POST
/PATCH
/DELETE /contacts
+ /restore
, /merge
|
disc:read
| Læse DISC-analyser og kommunikationsråd | GET /disc-analyses
|
insights:read
| Læse samtaleindsigt | GET /conversation-insights
|
messages:read
| Læse emails og opkald | GET /email-messages
, GET /phone-calls
|
follow-ups:read
| Læse follow-ups | GET /follow-ups
|
follow-ups:write
| Oprette og opdatere follow-ups | POST /follow-ups
+ /complete
, /dismiss
|
webhooks:read
| Læse webhook-opsætning og leveringslog | GET /webhooks
+ /deliveries
|
webhooks:write
| Administrere webhooks | POST
/PATCH
/DELETE /webhooks
+ /test
, /rotate-secret
|
analyze
| Køre ad-hoc AI-analyser | POST /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
på /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
.
- Send brugeren til
GET https://customerflow.dk/oauth/authorizemed query-parametrene nedenfor. Er brugeren ikke logget ind, sendes de først gennem CustomerFlows login (inkl. Microsoft SSO) og tilbage igen. - Brugeren ser en samtykke-skærm med de scopes I beder om, og godkender eller afviser.
- Ved godkendelse omdirigeres brugeren til jeres
redirect_urimed?code=...&state=.... Ved afvisning følger?error=access_denied(ogerror_code=api_access_requiredhvis kontoen ikke har API-adgang). - 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
:
| Parameter | Note |
|---|---|
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:
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):
{
"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_secreti body. Offentlige klienter (rene PKCE) sender intet secret. - Forny:
POST /api/oauth/tokenmedgrant_type: refresh_token,client_idogrefresh_token. Refresh-tokens roteres ved hver fornyelse (genbrug af et gammelt token tilbagekalder forbindelsen). Refresh-token gælder 30 dage. - Tilbagekald:
POST /api/oauth/revokemed{ "token": "..." }(access- eller refresh-token). Returnerer altid{ "revoked": true }og lækker ikke om tokenet fandtes. - Token-endpointet svarer med
error.codeved fejl, fxunsupported_grant_type(400),invalid_clientellerinvalid_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.
{
"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/0ellertrue/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 returnerer422 validation_failedmed en liste over tilladte værdier. - Fritekst (
qpå kontakter): matcher modname,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
:
{
"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 medq,primary_type,tag,is_blocked,do_not_call,updated_since.GET /v1/contacts/{id}POST /v1/contacts→201 CreatedPATCH /v1/contacts/{id}DELETE /v1/contacts/{id}→204 No Content. Soft delete: sætterdeleted_at; relaterede records bevares.POST /v1/contacts/{id}/restorePOST /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 sat på POST
.
| Felt | Type | Note |
|---|---|---|
name
| string (1-255) | Valgfrit. |
email
| string | Valgfrit. Skal være valid email. |
phone
| string (E.164) | Normaliseres automatisk (fx +4520202020
). |
company
| string | Valgfrit. |
cvr_number
| string (max 8) | Dansk CVR-nummer. |
notes
| string | Fritekst. |
tags
| string[] | Fx ["vip", "inbound"]
. |
is_blocked
| boolean | Block-flag. |
do_not_call
| boolean | Markerér som "ring ikke". |
recording_enabled
| boolean | Per-kontakt override af opkalds-optagelse. |
assigned_user_id
| integer | Kun ved POST
+ kun teamejere. Skal være medlem af eget team, ellers 422
. |
Eksempel: opret en kontakt.
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
.
{
"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 medcontact_id,primary_type(D|I|S|C),from,to.GET /v1/disc-analyses/{id}: inkluderer dekrypteredereply_suggestions,communication_tipsogrationale.
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 medstatus(pending|completed|dismissed),contact_id.GET /v1/follow-ups/{id}POST /v1/follow-ups→201. Skrivbare felter nedenfor.POST /v1/follow-ups/{id}/completePOST /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.
| Felt | Type | Note |
|---|---|---|
contact_id
| integer | Valgfrit. Sat, skal kontakten være synlig for dig (ellers 404
). Udeladt bliver follow-up'en kontaktløs. |
type
| string | Valgfrit. reminder
eller task
. Standard er task
. |
title
| string (max 255) | Valgfrit. Kort overskrift. |
suggested_date
| date | Påkrævet. I dag eller senere (YYYY-MM-DD
). |
due_at
| datetime | Valgfrit. Nu eller senere. Præcist forfaldstidspunkt (ISO 8601). |
suggested_message
| string (max 5000) | Valgfrit. Fritekst. |
Respons: 201 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 medcontact_id,conversation_id.GET /v1/conversation-insights/{id}
Emails
GET /v1/email-messages: filtrér medcontact_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 medcontact_id,direction(outbound|inbound),status,from,to.GET /v1/phone-calls/{id}: inkluderersegments[]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, hverbodymin 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:
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):
{
"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/webhooks→201. Body:name(max 80),url(HTTPS, max 2048),events(array, mindst én). Returnerersigning_secreti plaintext én gang.GET /v1/webhooks/{id}: detail. Indeholder ikke signing_secret.PATCH /v1/webhooks/{id}: opdatérname,url,eventselleris_active.DELETE /v1/webhooks/{id}→204.POST /v1/webhooks/{id}/test: sender etping-event. Test-leveringer tæller ikke mod auto-disable.POST /v1/webhooks/{id}/rotate-secret: genererer nysigning_secretog invaliderer den gamle øjeblikkeligt. Responsen har samme shape somPOSTog 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.
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
):
{
"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
{
"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)
{
"id": 412,
"deleted_at": "2026-05-21T09:45:00+00:00"
}
disc.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
{
"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
{
"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:
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
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
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
2xxmed 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
urldirekte på din endelige endpoint. Et3xx-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 leveringendead. - Andre 4xx (400, 401, 403, 404, 422):
faileduden 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
.
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"details": {
"email": ["must be a valid email"]
}
}
}
| Code | Status | Betydning |
|---|---|---|
bad_request
| 400 | Forespørgslen er ugyldig eller mangler påkrævede parametre. |
invalid_token
| 401 | Bearer mangler, er ugyldig, eller tilbagekaldt. |
subscription_inactive
| 403 | Nøglens ejer har intet aktivt abonnement. |
api_access_required
| 403 | API-adgang er ikke aktiveret på kontoen. Kontakt salg for at få det slået til. |
forbidden
| 403 | Auth ok, men handlingen er ikke tilladt. |
insufficient_scope
| 403 | Tokenet mangler et krævet scope. De manglende scopes står i details.required_scopes
. Se Scopes & OAuth. |
not_found
| 404 | Ressource findes ikke (eller du har ikke adgang). |
method_not_allowed
| 405 | HTTP-metoden er ikke tilladt på dette endpoint. |
conflict
| 409 | Fx merge mellem inkompatible kontakter. |
validation_failed
| 422 | Body fejler validation. Se details
. |
rate_limited
| 429 | For mange requests. Se Retry-After
. |
fair_use_exceeded
| 429 | Månedlig API fair-use-grænse nået (fælles pr. team, 1000 pr. seat). Retry-After
= sekunder til måneds-rollover. |
server_error
| 500 | Uventet fejl. Prøv igen senere, ellers kontakt support. |
upstream_unavailable
| 502 | Anthropic returnerede fejl. Intet forbrugt. |
upstream_timeout
| 504 | Anthropic-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