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

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
  }
}

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.

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
    },
    "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 med contact_id , primary_type (D|I|S|C), from , to .
  • GET /v1/disc-analyses/{id} : inkluderer dekrypterede reply_suggestions , communication_tips og rationale .

Follow-ups

  • GET /v1/follow-ups : filtrér med status (pending |completed |dismissed ), contact_id .
  • GET /v1/follow-ups/{id}
  • POST /v1/follow-ups201 . Body: contact_id (int), suggested_date (i dag eller senere), suggested_message (valgfri).
  • POST /v1/follow-ups/{id}/complete
  • POST /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 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.

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..."
    }
  }
}

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..."
  },
  "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
}

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:

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
invalid_token 401Bearer mangler, er ugyldig, eller tilbagekaldt.
subscription_inactive 403Nøglens ejer har intet aktivt abonnement.
enterprise_required 403API'et kræver Enterprise-planen. Kontakt salg for at opgradere.
forbidden 403Auth ok, men handlingen er ikke tilladt.
not_found 404Ressource findes ikke (eller du har ikke adgang).
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

Tilpas din kommunikation til hver kunde

Kom i gang med CustomerFlow og få DISC-indsigt på dine kunder fra første uge.

Kom hurtigt i gang · Opsig når som helst