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.
- 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 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
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 scopesBeder 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
:
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.
api-read
120 req/minAlle GET-endpointsapi-write
60 req/minPOST / PATCH / DELETEapi-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
:
{
"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
.
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.
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.
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
.
{
"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.
Verificér signatur
HMAC-SHA256 over <t>.<raw-body>
med din signing secret. Sammenlign i konstant tid og afvis tidsstempler ældre end 5 minutter.
Levering og retry
Vi venter højst 10 sekunder på et 2xx
. Ellers gentages leveringen med backoff [1m, 5m, 30m, 2h, 12h]
, i alt 5 forsøg.
Auto-disable
En webhook ældre end 48 timer med 25+ fejlede leveringer i træk og ingen succes de seneste 24 timer deaktiveres automatisk. Test-leveringer tæller ikke med.
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
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 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
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 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 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"]
}
}
}
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.
Har I allerede API-adgang? Opret en nøgle under Indstillinger · API