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-nøgler kan kun oprettes på Enterprise-planen. Teammedlemmer skal bede deres teamejer om at etablere integrationer.
Autentificering
Alle endpoints kræver en gyldig API-nøgle og en aktiv Enterprise-plan. API-adgang er forbeholdt Enterprise - kontakt salg for at opgradere.
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
}
}
role
er customer
, admin
eller super_admin
. team_id
er null for solo-brugere.
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
},
"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
}
}
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.
Follow-ups
GET /v1/follow-ups: filtrér medstatus(pending|completed|dismissed),contact_id.GET /v1/follow-ups/{id}POST /v1/follow-ups→201. Body:contact_id(int),suggested_date(i dag eller senere),suggested_message(valgfri).POST /v1/follow-ups/{id}/completePOST /v1/follow-ups/{id}/dismiss
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.
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..."
}
}
}
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..."
},
"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
}
Andre events (follow_up.created
, follow_up.completed
, 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
. 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 |
|---|---|---|
invalid_token
| 401 | Bearer mangler, er ugyldig, eller tilbagekaldt. |
subscription_inactive
| 403 | Nøglens ejer har intet aktivt abonnement. |
enterprise_required
| 403 | API'et kræver Enterprise-planen. Kontakt salg for at opgradere. |
forbidden
| 403 | Auth ok, men handlingen er ikke tilladt. |
not_found
| 404 | Ressource findes ikke (eller du har ikke adgang). |
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