
# Kontakt-API

En kontakt er en enkelt person, du sender beskeder til — deres navn, telefonnummer, e-mail, kanal, tags, brugerdefinerede felter samt de lister og kampagner, de tilhører. Kontakt-API'et giver dig mulighed for at oprette kontakter, slå dem op, opdatere dem, tilføje tags, importere dem i bulk og fjerne dem, alt sammen uden at bruge dashboardet.

Alle stier på denne side er relative til basis-URL'en:

```
https://api.youraiconnector.com/v1
```

Så `/contacts` betyder `https://api.youraiconnector.com/v1/contacts`.

> **Er du ny til API'et?** Læs [API-adgang](../integrations/api-access.md) først — den dækker, hvordan du genererer din API-nøgle, de tre måder at godkende på, hastighedsbegrænsninger og fejlformatet. Alt på denne side forudsætter, at du allerede har en fungerende API-nøgle.

---

## Om kontakt-ID'er

Hver kontakt har et unikt ID. Det ID, du får tilbage, når du **opretter** en kontakt (i `data.contactId`), er det samme ID, som du bruger alle andre steder — til at hente, opdatere, tilføje tags, sende en besked eller slette den pågældende kontakt. Gem det én gang og genbrug det.

Du behøver ikke at oprette en kontakt for at få dens ID. Du kan også slå et ID op via telefonnummer eller e-mail (se [Hent en kontakt](#get-a-contact-by-phone-or-email)), eller gennemse alle dine kontakter (se [List kontakter](#list-contacts)). Hver af disse returnerer det samme ID.

---

## Opret en kontakt

`POST /contacts`

Tilføjer en ny kontakt til din konto. Et **telefonnummer med landekode er påkrævet** — en e-mail alene er ikke nok. Alt andet er valgfrit.

Du kan valgfrit tilføje den nye kontakt direkte til en eller flere lister med `listId` (en enkelt liste) eller `listIds` (et array). Hvis begge sendes, vinder `listIds`.

Ethvert felt, du sender, som ikke er et af standardfelterne til oprettelse, der er angivet i tabellen **Opret en kontakt** nedenfor (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`), gemmes automatisk som et **brugerdefineret felt** — så en flad payload fra et værktøj som Make eller Zapier fungerer uden indlejring. Du kan også sende et eksplicit `custom_fields`-objekt.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `phoneNumber` | Ja | Kontaktens telefonnummer med landekode (f.eks. `+15551234567`). |
| `firstName` | Nej | Fornavn. |
| `lastName` | Nej | Efternavn. |
| `email` | Nej | E-mailadresse. |
| `channel` | Nej | Beskedkanal. En af `whatsapp`, `sms`, `whatsapp_web`. Standard er `whatsapp`. |
| `is_bot_active` | Nej | Om AI-assistenten svarer denne kontakt. Standard er `true`. |
| `is_private` | Nej | Marker kontakten som privat. Når `true`, er AI-assistenten slået fra for dem. Standard er `false`. |
| `lead_profile` | Nej | Fritekstnoter om emnet. |
| `listId` | Nej | Et enkelt liste-ID, som kontakten skal tilføjes til. |
| `listIds` | Nej | Et array af liste-ID'er, som kontakten skal tilføjes til (har forrang over `listId`). |
| `custom_fields` | Nej | Et objekt med dine egne nøgle/værdi-felter. Du kan også sende disse som top-level nøgler. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumber": "+15551234567",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "is_bot_active": true,
    "listIds": ["list123", "list456"]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/contacts", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phoneNumber: "+15551234567",
    firstName: "Jane",
    lastName: "Smith",
    email: "jane@example.com",
    is_bot_active: true,
    listIds: ["list123", "list456"],
  }),
});
const data = await res.json();
console.log(data.data.contactId);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "phoneNumber": "+15551234567",
        "firstName": "Jane",
        "lastName": "Smith",
        "email": "jane@example.com",
        "is_bot_active": True,
        "listIds": ["list123", "list456"],
    },
)
print(res.json()["data"]["contactId"])
```

**Svar**

```json
{
  "success": true,
  "data": {
    "message": "Successfully created new contact",
    "contactId": "contact_abc123",
    "listsAdded": ["list123", "list456"]
  }
}
```

Den nye kontakts ID findes på `data.contactId`. De lister, den blev tilføjet til, returneres i `data.listsAdded`.

> **Dubletter oprettes ikke.** Hvis en kontakt med det samme telefonnummer allerede findes, opretter eller returnerer opkaldet til oprettelse den **ikke**. Svaret kommer tilbage med HTTP-status `200` og en `error_code` på `409` i brødteksten, så forgrening bør ske på `error_code` frem for på HTTP-status:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> For at arbejde med en eksisterende kontakt efter en `error_code` på `409`, skal du slå den op med [Hent en kontakt via telefon eller e-mail](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — og genbruge det ID, den returnerer.

> **Tilsvarende WhatsApp-stavemåder tæller som det samme nummer.** Nogle lande har to gyldige stavemåder for den samme mobiltelefonlinje, og WhatsApp kan rapportere begge: Mexico (`+52…` og den ældre `+521…`), Brasilien (med eller uden det niende ciffer) og Argentina (med eller uden `9` efter `+54`). Dubletkontrollen ved oprettelse og `GET /contacts?phoneNumber=` matcher på tværs af begge stavemåder, så du får den eksisterende kontakt tilbage, uanset hvilken form du sender. Det `phone_number`, der er gemt på kontakten, bliver aldrig overskrevet.

---

## Hent en kontakt via telefon eller e-mail

`GET /contacts?phoneNumber=...` eller `GET /contacts?email=...`

Slår en enkelt kontakt op og returnerer det fulde, berigede kontaktobjekt — inklusive dens lister, tags og kampagner opløst til `{ id, name }`-par, plus den sidst udvekslede besked.

Angiv **enten** `phoneNumber` (i internationalt format) **eller** `email`. Hvis du ikke angiver nogen af dem, skifter dette samme slutpunkt i stedet til [List kontakter](#list-contacts)-tilstand.

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?phoneNumber=%2B15551234567&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const phone = encodeURIComponent("+15551234567");
const res = await fetch(`https://api.youraiconnector.com/v1/contacts?phoneNumber=${phone}`, {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.contact);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"phoneNumber": "+15551234567"},
)
print(res.json()["contact"])
```

**Svar**

```json
{
  "success": true,
  "contactId": "contact_abc123",
  "contact": {
    "id": "contact_abc123",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "phoneNumber": "+15551234567",
    "channel": "whatsapp",
    "isBotActive": true,
    "isPrivate": false,
    "doNotDisturb": false,
    "lead_profile": null,
    "avatarUrl": "https://example.com/photo.jpg",
    "customFields": {},
    "lists": [{ "id": "list123", "name": "VIP customers" }],
    "tags": [{ "id": "tagHotLead", "name": "Hot lead" }],
    "campaigns": [{ "id": "campaign789", "name": "Spring promo" }],
    "currentCampaign": { "id": "campaign789", "name": "Spring promo" },
    "lastMessage": {
      "direction": "inbound",
      "body": "Sounds good, thanks!",
      "status": "received",
      "timestamp": "2026-06-09T10:21:00.000Z"
    }
  }
}
```

Kontakt-ID'et returneres både på øverste niveau (`contactId`) og inde i objektet (`contact.id`). Hvis intet matcher, får du en `404` med `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** er kontaktens profilbillede, hentet fra WhatsApp eller Meta, når de sender dig en besked. Det er skrivebeskyttet: Du kan ikke indstille det, og det er `null` for kontakter, der ikke har et billede, eller som kontakter dig via en kanal, der ikke deler et. Betragt linket som midlertidigt frem for at gemme det, da nogle af disse billedlinks udløber og opdateres automatisk. (I liste-slutpunktet nedenfor kaldes den samme værdi `avatar_url`.)

> **Telefonnumre i URL'er.** Et `+`-tegn i en forespørgselsstreng skal være URL-kodet som `%2B`, ellers læses det som et mellemrum. Eksemplerne ovenfor gør dette for dig.

---

## Hent en kontakt via ID

`GET /contacts/{contactId}`

Når du allerede har en kontakts ID, kan du hente den direkte. Svarets form er identisk med opslaget ovenfor.

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.contact);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["contact"])
```

Et kontakt-ID, der ikke findes på din konto, returnerer en `404`.

---

## Hent kontaktstatistik

`GET /contacts/{contactId}/stats`

Returnerer samlet beskedstatistik for én kontakt: totaler, AI- kontra menneskelige svar, brugte kreditter og tidsstempler for første/sidste besked.

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.totalMessages, data.creditsUsed);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
print(data["totalMessages"], data["creditsUsed"])
```

**Svar**

```json
{
  "success": true,
  "totalMessages": 48,
  "sent": 21,
  "received": 27,
  "aiReplies": 18,
  "humanReplies": 3,
  "creditsUsed": 34,
  "botMessageCount": 18,
  "firstMessageAt": "2026-05-01T09:00:00.000Z",
  "lastMessageAt": "2026-06-09T10:21:00.000Z"
}
```

`botMessageCount` er den samme AI-beskedtæller, som "nulstil"-knappen i appen for en kontakt nulstiller. `creditsUsed` er den løbende kredit-total for denne kontakt, ikke kun tallene for dette svar. Et kontakt-ID, der ikke findes på din konto, returnerer en `404`.

---

## List kontakter

`GET /contacts`

Kald `GET /contacts` med **hverken** `phoneNumber` eller `email` for at gennemse alle dine kontakter, med de nyeste først. Hver side returnerer kompakte kontaktoversigter (lister, tags og kampagner returneres som ID-arrays i stedet for fulde objekter) og en `next_cursor`.

| Forespørgselsparameter | Beskrivelse |
|---|---|
| `limit` | Sidestørrelse. Standard er 50, maksimum 100. |
| `cursor` | `next_cursor`-værdien fra den forrige side. Udelad den på den første side. |
| `listId` | Valgfri. Returner kun kontakter, der tilhører denne liste. |

For at gennemgå hver side: foretag det første kald uden en markør (cursor), og fortsæt derefter med at sende den returnerede `next_cursor` tilbage som `cursor`. **Stop når `next_cursor` er `null`** — det betyder, at der ikke er flere resultater.

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?limit=50&apiKey=YOUR_API_KEY"

# next page:
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?limit=50&cursor=contact_abc123&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
async function listAllContacts() {
  const all = [];
  let cursor = null;
  do {
    const url = new URL("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, { headers: { "X-API-Key": "YOUR_API_KEY" } });
    const data = await res.json();
    all.push(...data.contacts);
    cursor = data.next_cursor;
  } while (cursor);
  return all;
}
```

**Python**

```python
import requests

def list_all_contacts():
    all_contacts = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
            headers={"X-API-Key": "YOUR_API_KEY"},
            params=params,
        )
        data = res.json()
        all_contacts.extend(data["contacts"])
        cursor = data["next_cursor"]
        if not cursor:
            break
    return all_contacts
```

**Svar**

```json
{
  "success": true,
  "contacts": [
    {
      "id": "contact_abc123",
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane@example.com",
      "phone_number": "+15551234567",
      "channel": "whatsapp",
      "is_bot_active": true,
      "is_private": false,
      "do_not_disturb": false,
      "avatar_url": "https://example.com/photo.jpg",
      "custom_fields": {},
      "created_at": "2026-06-01T09:00:00.000Z",
      "list_ids": ["list123"],
      "tag_ids": ["tagHotLead"],
      "campaign_ids": ["campaign789"],
      "current_campaign_id": "campaign789"
    }
  ],
  "next_cursor": "contact_abc123"
}
```

::: note
**Bemærk:** Filtrering efter en `listId`, der ikke findes på din konto, returnerer en `404`. En ugyldig `cursor` returnerer en `400`.
:::


---

## Tæl kontakter

`GET /contacts/count`

Returnerer hvor mange kontakter der matcher et filter, plus en opdeling pr. kanal, uden at skulle gennemse dem side for side. Dette er det rette kald til ethvert "hvor mange"-spørgsmål — en dashboard-flise, en automatisering eller når du spørger Champ. Alle filtre er valgfrie, og kombination af flere indsnævrer optællingen (en kontakt skal matche hver enkelt, du sender).

| Forespørgselsparameter | Beskrivelse |
|---|---|
| `agentId` | Kun kontakter tildelt denne AI-agent. Send `none` for kontakter uden tildelt agent (disse besvares af kanalens standardagent). |
| `channel` | Kun kontakter på denne kanal, f.eks. `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Kun kontakter med dette tag, efter tag-**navn** (store/små bogstaver er underordnet). Et tag-navn, du ikke har, returnerer en `404`. |
| `listId` | Kun kontakter på denne liste. |
| `botActive` | `true` eller `false` — kun kontakter, hvis AI-assistent er tændt eller slukket. |
| `status` | Kun kontakter med denne status, f.eks. `Lead`. |
| `rules` | Et URL-kodet JSON-regelobjekt, der bruger samme form som en smart liste (se [The `smart_rules` shape](#the-smart_rules-shape) længere nede). Kan ikke kombineres med de andre filtre. |

Send slet intet filter, og du får det samlede antal kontakter på din konto.

**cURL**

```bash
# everything
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count?apiKey=YOUR_API_KEY"

# only the contacts one agent handles on Messenger
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count?agentId=agent_xyz789&channel=messenger&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count");
url.searchParams.set("agentId", "agent_xyz789");
url.searchParams.set("channel", "messenger");

const res = await fetch(url, { headers: { "X-API-Key": "YOUR_API_KEY" } });
const data = await res.json();
console.log(data.total);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"agentId": "agent_xyz789", "channel": "messenger"},
)
data = res.json()
print(data["total"])
```

**Svar**

```json
{
  "success": true,
  "total": 3423,
  "by_channel": { "messenger": 2744, "instagram": 667, "none": 12 },
  "filters": { "agentId": "agent_xyz789" }
}
```

`by_channel` opdeler det samme totalantal pr. kanal; kontakter, der ikke er på nogen kanal, tælles under `none`. `filters` sender de anvendte filtre retur, så du kan kontrollere, at kaldet gjorde, hvad du forventede.

::: note
**Bemærk:** Hvis du sender `rules` sammen med et andet filter, eller en `rules`-værdi, der ikke er gyldig JSON, returneres en `400`. Et tag-navn eller liste-ID, der ikke findes på din konto, returnerer en `404`.
:::


---

## Opdater en kontakt

`PUT /contacts/{contactId}`

Opdaterer en eksisterende kontakt. Kun de felter, du inkluderer, ændres — udelad alt, du ikke ønsker at røre ved. Du skal sende mindst ét felt, ellers får du en `400` ("Ingen felter at opdatere").

| Felt | Beskrivelse |
|---|---|
| `firstName` | Fornavn. |
| `lastName` | Efternavn. |
| `email` | E-mailadresse. |
| `is_bot_active` | Hvorvidt AI-assistenten svarer denne kontakt. |
| `is_private` | Markér som privat. Hvis denne sættes til `true`, slås AI-assistenten også fra. |
| `do_not_disturb` | Sæt automatisk kontakt til denne person på pause. Stopper også AI'en fra at svare. |
| `follow_ups_disabled` | Stop alle automatiske opfølgninger for denne kontakt (hurtig, cyklus og kold lead), mens AI'en fortsætter med at svare på beskeder, de sender. Nyttigt når nogen har købt. Forbliver slået fra, indtil du sætter den tilbage til `false`. |
| `lead_profile` | Fritekstnoter om leadet. |
| `custom_fields` | Et objekt med brugerdefinerede felter. **Flettes pr. nøgle** — kun de nøgler, du sender, bliver skrevet; resten af de eksisterende brugerdefinerede felter bevares. Du kan også sende nøgler til brugerdefinerede felter på øverste niveau. |

**cURL**

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Jane", "do_not_disturb": true }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  method: "PUT",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ firstName: "Jane", do_not_disturb: true }),
});
const data = await res.json();
console.log(data.message);
```

**Python**

```python
import requests

res = requests.put(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"firstName": "Jane", "do_not_disturb": True},
)
print(res.json()["message"])
```

**Svar**

```json
{
  "success": true,
  "message": "Contact updated successfully"
}
```

> **Brugerdefinerede felter flettes, ikke erstattes.** Afsendelse af `{ "custom_fields": { "tier": "gold" } }` indstiller kun `tier` — alle andre brugerdefinerede felter på kontakten forbliver præcis, som de var. For at fjerne et brugerdefineret felt helt på tværs af alle kontakter, skal du bruge [Slet et brugerdefineret felt](#delete-a-custom-field).

---

## Tilføj eller fjern tags

`POST /contacts/{contactId}/tags`

Tilføjer og/eller fjerner tags på en enkelt kontakt i ét kald. Send tag-**ID'er** i `addTagIds` og `removeTagIds`. Mindst ét af de to skal være ikke-tomt.

Tags skal allerede eksistere på din konto — opret dem først via [tags-endpointet](reference.md). Hvis kontakten eller et refereret tag ikke findes, får du en `404`.

| Felt | Beskrivelse |
|---|---|
| `addTagIds` | Array af tag-id'er, der skal tilføjes til kontakten. |
| `removeTagIds` | Array af tag-id'er, der skal fjernes fra kontakten. |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "addTagIds": ["tagHotLead"], "removeTagIds": ["tagColdLead"] }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    addTagIds: ["tagHotLead"],
    removeTagIds: ["tagColdLead"],
  }),
});
const data = await res.json();
console.log(data.added, data.removed);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"addTagIds": ["tagHotLead"], "removeTagIds": ["tagColdLead"]},
)
data = res.json()
print(data["added"], data["removed"])
```

**Svar**

```json
{
  "success": true,
  "contact_id": "contact_abc123",
  "added": 1,
  "removed": 1
}
```

---

## Administrer dit tag-bibliotek

Disse slutpunkter administrerer selve tagget — omdøbning eller sletning af det på din konto — i modsætning til at tilføje eller fjerne et tag på en kontakt (se [Tilføj eller fjern tags](#add-or-remove-tags) ovenfor). Hvert tag på din konto har et ID (`tagId`): det, der vises i dit dashboards tag-administrator, og det, der returneres som `data.tag_id`, når du opretter et tag med `POST /tags` og en JSON-krop på `{ "name": "..." }` (ingen `phoneNumber`, `email` eller `contactId`).

### Opdater et tag

`PUT /tags/{tagId}`

Send kun de felter, du ændrer.

| Felt | Beskrivelse |
|---|---|
| `name` | Taggets navn. |

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags/tagHotLead?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Hot lead (Q3)" }'
```

**Svar**

```json
{ "success": true, "tag_id": "tagHotLead" }
```

Et `tagId`, der ikke findes på din konto, returnerer en `404`.

### Slet et tag

`DELETE /tags/{tagId}`

Sletter ét tag efter ID. **Dette kan ikke fortrydes** — kontakter, der bærer tagget, mister det blot. Sletning af et tag, der allerede er væk (eller aldrig har eksisteret), returnerer `200` med `deleted: 0` i stedet for en `404`, da der ikke er noget at tælle op.

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags/tagColdLead?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{ "success": true, "deleted": 1 }
```

### Slet flere tags på én gang

`DELETE /tags`

| Felt | Beskrivelse |
|---|---|
| `tagIds` | Array af tag-id'er, der skal slettes (maks. 1000). |

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tagIds": ["tagColdLead", "tagUnsubscribed"] }'
```

**Svar**

```json
{ "success": true, "deleted": 2 }
```

ID'er, der ikke findes, eller som tilhører en anden konto, springes over uden fejlmeddelelse og tælles ikke med i `deleted`.

---

## Masseindstil et flag

`POST /contacts/bulk-flag`

Indstiller ét boolesk flag på mange kontakter på én gang. Op til 500 kontakt-id'er pr. anmodning. Id'er, der ikke findes på din konto, springes over og tælles i `skipped`.

| Felt | Beskrivelse |
|---|---|
| `contactIds` | Array af kontakt-id'er, der skal opdateres (maks. 500). |
| `field` | Hvilket flag der skal indstilles. Et af `bot_active` (AI-assistent til/fra), `dnd` (pause automatiseret kontakt), `spam`, `private`. |
| `value` | Den booleske værdi, flaget skal indstilles til. |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactIds": ["contactId1", "contactId2"],
    "field": "bot_active",
    "value": false
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contactIds: ["contactId1", "contactId2"],
    field: "bot_active",
    value: false,
  }),
});
const data = await res.json();
console.log(data.updated, data.skipped);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "contactIds": ["contactId1", "contactId2"],
        "field": "bot_active",
        "value": False,
    },
)
data = res.json()
print(data["updated"], data["skipped"])
```

**Svar**

```json
{
  "success": true,
  "updated": 2,
  "skipped": 0
}
```

---

## Masseimport af kontakter

`POST /contacts/import`

Opretter op til 500 kontakter i ét kald fra et JSON-array. Hver post kræver et `phone_number` i internationalt format; alt andet er valgfrit. Poster med ugyldige telefonnumre eller ikke-understøttede kanaler **springes over** (oprettes ikke), og hver overspringet post rapporteres med sit indeks og årsag — så du kun behøver at rette fejlene og prøve igen.

Telefonnumre, der allerede findes på din konto, springes som standard over som `duplicate`. Send `updateExisting: true` for at **opdatere** disse kontakter i stedet: felterne i posten overskriver kontaktens (`first_name`, `last_name`, `email`, `lead_profile` og `custom_fields` flettes nøgle for nøgle), `tags` tilføjes, og kontakten føjes til `listId`. Kanal, telefonnummer og bot-flag ændres aldrig på en eksisterende kontakt.

Du kan valgfrit tilføje hver importeret (eller opdateret) kontakt til en liste med `listId`, angive en `defaultChannel` for poster, der ikke angiver en, og tagge poster med `tags` (tagnavne — manglende tags oprettes, eksisterende matches uafhængigt af store/små bogstaver).

**Top-niveau felter**

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `contacts` | Ja | Array af kontaktposter (maks. 500). |
| `listId` | Nej | Liste, som hver importeret (og opdateret) kontakt skal føjes til. Skal være en liste på din konto. |
| `defaultChannel` | Nej | Kanal anvendt på poster, der udelader `channel`. En af `whatsapp`, `sms`, `whatsapp_web`. Standard er `whatsapp`. |
| `updateExisting` | Nej | `true` for at opdatere kontakter, hvis telefonnummer allerede findes, i stedet for at springe dem over som `duplicate`. Standard er `false`. |

**Felter pr. post**

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `phone_number` | Ja | Telefonnummer i internationalt format (et indledende `+` tilføjes, hvis det mangler). |
| `first_name` | Nej | Fornavn. |
| `last_name` | Nej | Efternavn. |
| `email` | Nej | E-mailadresse. |
| `channel` | Nej | En af `whatsapp`, `sms`, `whatsapp_web`. Falder tilbage på `defaultChannel`. |
| `is_bot_active` | Nej | Hvorvidt AI-assistenten svarer. Standard er `true`. |
| `is_private` | Nej | Marker som privat. Standard er `false`. |
| `lead_profile` | Nej | Fritekst-notater om emnet. |
| `custom_fields` | Nej | Objekt med brugerdefinerede feltnøgler og værdier. |
| `tags` | Nej | Array af tagnavne (en enkelt `"a; b"` streng virker også). Tags, der ikke findes, oprettes; eksisterende matches uden hensyntagen til store/små bogstaver. Maks. 25 pr. post. |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone_number": "+12025551234", "first_name": "Ann", "last_name": "Lee", "tags": ["vip", "newsletter"] },
      { "phone_number": "+12025551235", "first_name": "Bob" }
    ],
    "listId": "list123",
    "defaultChannel": "whatsapp_web",
    "updateExisting": true
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contacts: [
      { phone_number: "+12025551234", first_name: "Ann", last_name: "Lee", tags: ["vip", "newsletter"] },
      { phone_number: "+12025551235", first_name: "Bob" },
    ],
    listId: "list123",
    defaultChannel: "whatsapp_web",
    updateExisting: true,
  }),
});
const data = await res.json();
console.log(`Imported ${data.imported}, updated ${data.updated}, skipped ${data.skipped.length}`);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "contacts": [
            {"phone_number": "+12025551234", "first_name": "Ann", "last_name": "Lee", "tags": ["vip", "newsletter"]},
            {"phone_number": "+12025551235", "first_name": "Bob"},
        ],
        "listId": "list123",
        "defaultChannel": "whatsapp_web",
        "updateExisting": True,
    },
)
data = res.json()
print(f"Imported {data['imported']}, updated {data['updated']}, skipped {len(data['skipped'])}")
```

**Svar**

```json
{
  "success": true,
  "imported": 2,
  "contact_ids": ["contact_abc123", "contact_def456"],
  "updated": 0,
  "updated_contact_ids": [],
  "skipped": []
}
```

Hvis nogle poster ikke kan oprettes, vises de i `skipped` med årsagen (her uden `updateExisting`, så det eksisterende nummer springes over):

```json
{
  "success": true,
  "imported": 1,
  "contact_ids": ["contact_abc123"],
  "updated": 0,
  "updated_contact_ids": [],
  "skipped": [
    { "index": 1, "phone_number": "+12025551235", "reason": "duplicate" }
  ]
}
```

Med `updateExisting: true` rapporterer den samme anmodning den eksisterende kontakt under `updated` / `updated_contact_ids` i stedet.

Mulige årsager til spring over: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Planbegrænsninger.** Hvis din plans kontaktgrænse ikke tillader så mange nye kontakter, afvises hele anmodningen på forhånd med en `403`. Hvis grænsen nås undervejs, returneres de resterende poster som sprunget over med årsagen `contact_limit_reached`.

---

## Importér kontakter fra en CSV-fil

Ved importer, der er større end hvad [bulk import](#bulk-import-contacts) understøtter (op til cirka 50.000 rækker), skal du køe et asynkront importjob mod en CSV-fil, der allerede ligger i din kontos lager, og derefter polle det, indtil det er fuldført.

### Start importen

`POST /contacts/import-csv`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `csvStoragePath` | Ja | Lagersti til CSV-filen under `users/{your account id}/imports/`, der slutter på `.csv`. |
| `listName` | Ja | Opretter (eller genbruger) en liste med dette navn og tilføjer alle importerede kontakter til den. |
| `existingListRefs` | Nej | Array af eksisterende liste-id'er, som alle importerede kontakter også skal tilføjes til. |
| `defaultChannel` | Nej | Kanal anvendt på rækker, der ikke angiver en. |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csvStoragePath": "users/abc123/imports/leads.csv",
    "listName": "Webinar signups"
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csvStoragePath: "users/abc123/imports/leads.csv",
    listName: "Webinar signups",
  }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "csvStoragePath": "users/abc123/imports/leads.csv",
        "listName": "Webinar signups",
    },
)
job_id = res.json()["job_id"]
```

**Svar** (`202` — importen er sat i kø, ikke færdig endnu)

```json
{
  "success": true,
  "job_id": "csvimp_abc123",
  "status": "queued"
}
```

> **Få filen ind i lageret.** Dette endepunkt starter og sporer importjobbet; det accepterer ikke selv en upload. CSV-filen skal allerede findes på `csvStoragePath`, før du kalder det — dashboardets egen CSV-importør gør dette som sit første skridt.

### Polling af importjobbet

`GET /contacts/import-csv/{jobId}`

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv/csvimp_abc123?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{
  "success": true,
  "job_id": "csvimp_abc123",
  "status": "completed",
  "imported": 812,
  "updated": 0,
  "skipped": 14,
  "errors": [],
  "error_message": null
}
```

`status` bevæger sig gennem `queued` → `processing` → `completed` eller `failed` med årsagen i `error_message`. Et `jobId`, der ikke findes på din konto, returnerer en `404`.

---

## Eksportér kontakter

Starter en asynkron CSV-eksport af dine kontakter og returnerer et job, som du kan polle for færdiggørelse.

### Start eksporten

`POST /contacts/export`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `listId` | Nej | Eksportér kun kontakter, der tilhører denne liste. |
| `contactIds` | Nej | Eksportér kun disse specifikke kontakt-id'er. |

Hvis begge udelades, eksporteres alle kontakter på din konto.

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "listId": "list123" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ listId: "list123" }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"listId": "list123"},
)
job_id = res.json()["job_id"]
```

**Svar** (`202` — eksporten er sat i kø)

```json
{
  "success": true,
  "job_id": "export_abc123",
  "status": "queued"
}
```

### Forespørg eksportjobbet

`GET /contacts/export/{jobId}`

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export/export_abc123?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{
  "success": true,
  "job_id": "export_abc123",
  "status": "completed",
  "export_id": "exp_xyz789",
  "contact_count": 812,
  "error_message": null
}
```

> Når `status` er `"completed"`, modtager du `export_id` og `contact_count`. Download af den genererede CSV-fil sker fra dit kontrolpanels Eksport-side.

---

## Send en besked til en kontakt

`POST /contacts/{contactId}/send-message`

Sender en besked til en eksisterende kontakt på den kanal, de allerede befinder sig på. Beskeden sættes i kø og leveres i baggrunden — svaret bekræfter, at den blev accepteret, ikke at den er blevet leveret endnu.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `body` | Ja | Teksten i beskeden, der skal sendes. |
| `mediaUrl` | Nej | URL til en mediefil, der skal vedhæftes. |
| `mediaContentType` | Nej | MIME-type for det vedhæftede medie (f.eks. `image/jpeg`). |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Hi! Your appointment is confirmed." }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ body: "Hi! Your appointment is confirmed." }),
});
const data = await res.json();
console.log(data.messageId);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"body": "Hi! Your appointment is confirmed."},
)
print(res.json()["messageId"])
```

**Svar**

```json
{
  "success": true,
  "messageId": "aB3dE5fG7hI9jK1lM2nO",
  "contactId": "contact_abc123",
  "channel": "whatsapp",
  "message": "Message created successfully. Delivery is being processed."
}
```

> **Kan ikke sende lige nu?** Hvis kontakten har aktiveret forstyr-ikke eller privat tilstand, eller ikke er på en kanal, der kan modtage udgående beskeder, afvises anmodningen med en `422` og en forklarende `error`.

For afsendelse via telefonnummer, Instagram-id eller anden kanalidentitet i stedet for et kontakt-id — og for mere om beskeder generelt — se [Messages API](messages.md).

---

## Tildel en AI-agent til en kontakt

`POST /contacts/{contactId}/assign-agent`

Flytter en eksisterende samtale til en anden AI-agent fra og med den næste besked. Det svarer til **Tildel AI-agent** i en chats menu, og det er det samme trin, som handlingen **Tildel AI-agent eller kampagne** i Automatiseringer bruger.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `agentId` | Ja | ID'et på den AI-agent, der skal overtage, eller `null` for at fjerne tildelingen, så samtalen går tilbage til din team-indbakke. |
| `triggerAIResponse` | Nej | `true` får den nyligt tildelte agent til at svare på kontaktens seneste ubesvarede beskeder med det samme. Standard er `false`. |

> **Vær forsigtig med `triggerAIResponse: true`** — den sender en besked til kontakten med det samme, så brug den kun, når du ønsker, at de skal kontaktes nu. På Messenger og Instagram fejler beskeden, hvis kontakten sidst skrev til dig for mere end 24 timer siden.

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "agent_xyz789" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ agentId: "agent_xyz789" }),
});
const data = await res.json();
console.log(data.data.agentId);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"agentId": "agent_xyz789"},
)
print(res.json()["data"]["agentId"])
```

**Svar**

```json
{
  "success": true,
  "data": {
    "contactId": "contact_abc123",
    "agentId": "agent_xyz789",
    "aiResponseTriggered": false
  }
}
```

> Agenten skal tilhøre den samme konto som kontakten; ellers afvises anmodningen med en `404` eller `403`. Find agent-ID'er på siden AI-agenter (hver agents URL slutter med dens ID).

---

## Tildel en AI-agent til mange kontakter

`POST /contacts/bulk-assign-agent`

Flytter mange samtaler til en anden AI-agent i ét kald — eller rydder tildelingen for dem alle med `null`. Det er udelukkende en routing-ændring: **ingen besked sendes, og agenten svarer ikke nogen**. Hver kontakt får blot den nye agent, næste gang de skriver. (Det er derfor, der ikke er nogen `triggerAIResponse` her.)

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `agentId` | Ja | Den AI-agent, der skal overtage, eller `null` for at rydde tildelingen. |
| `contactIds` | Én af de tre | Op til 500 kontakt-ID'er, der skal flyttes. |
| `filter` | Én af de tre | Vælg kontakterne på serveren i stedet for at angive dem, nyeste først. Bruger de samme nøgler som tælle-endepunktets filtre: `agentId` (eller `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Én af de tre | Et smart-liste-regelobjekt — se [The `smart_rules` shape](#the-smart_rules-shape). |
| `limit` | Nej | Hvor mange kontakter der skal flyttes i dette kald, når du vælger med `filter` eller `rules`. 1 til 500, standard er 500. |

Send præcis én af `contactIds`, `filter` eller `rules`.

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "agent_xyz789",
    "filter": { "agentId": "agent_abc123", "channel": "messenger" }
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: "agent_xyz789",
    filter: { agentId: "agent_abc123", channel: "messenger" },
  }),
});
const data = await res.json();
console.log(data.updated, data.remaining);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "agentId": "agent_xyz789",
        "filter": {"agentId": "agent_abc123", "channel": "messenger"},
    },
)
data = res.json()
print(data["updated"], data["remaining"])
```

**Svar**

```json
{
  "success": true,
  "agentId": "agent_xyz789",
  "matched": 3415,
  "updated": 500,
  "skipped": 0,
  "remaining": 2915,
  "filters": { "agentId": "agent_abc123" }
}
```

`matched` er hvor mange kontakter udvalget fandt i alt, `updated` hvor mange der blev flyttet af dette kald, `skipped` hvor mange af de ID'er, du sendte, der ikke blev fundet på din konto, og `remaining` hvor mange der stadig matcher nu, hvor kaldet er færdigt.

**Flytning af alle.** Da et kald flytter maksimalt 500 kontakter, kræver en stor gruppe nogle få kald. Brug et filter, der holder op med at matche en kontakt, når den er flyttet — for eksempel `filter: { "agentId": "agent_abc123" }` mens der tildeles til `agent_xyz789` — og gentag det præcis samme kald, indtil `remaining` kommer tilbage som `0`. Når du i stedet sender `contactIds`, er `remaining` altid `0`.

---

## Tildel en kontakt til en afdeling

`POST /contacts/{contactId}/department`

"Tildel dette lead til Salg" — placerer en kontakt under en navngiven afdeling og giver den som standard til den person i afdelingen, der i øjeblikket har færrest kontakter. Dette er adskilt fra [tildeling af en AI-agent](#assign-an-ai-agent-to-a-contact): en afdeling besvarer "hvilket team ejer dette," en agent besvarer "hvilken AI besvarer dette," og indstilling af den ene sletter aldrig den anden.

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `department_id` | Ja | Afdelingen, som kontakten skal placeres under. Send `null` for at rydde den. |
| `hand_to_member` | Nej | Giv også kontakten til den person i afdelingen, der har mindst at lave. Standard er `true`. Omfordeler aldrig en kontakt, som nogen allerede ejer. |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "department_id": "dept_sales" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ department_id: "dept_sales" }),
});
const data = await res.json();
console.log(data.assigned_to);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"department_id": "dept_sales"},
)
print(res.json()["assigned_to"])
```

**Svar**

```json
{
  "success": true,
  "department_id": "dept_sales",
  "assigned_to": "member_uid_123"
}
```

`assigned_to` er `null`, når kontakten allerede var ejet af nogen, eller du sendte `hand_to_member: false`.

---

## Link en kontakt på tværs af kanaler

"Fortsæt på WhatsApp" (eller SMS) finder eller opretter denne persons kontakt på en anden telefonbaseret kanal og linker de to sammen, så resten af appen genkender dem som den samme person.

### Link til en anden kanal

`POST /contacts/{contactId}/link-channel`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `channel` | Ja | Kanalen, der skal linkes til. En af `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Nej | Telefonnummer, der skal bruges på den nye kanal. Som standard bruges kildekontaktens eget nummer. |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/link-channel?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms" }'
```

**Svar**

```json
{
  "success": true,
  "data": {
    "contact_id": "contact_def456",
    "person_id": "person_xyz789",
    "created": true
  }
}
```

`created` fortæller dig, om en ny kontakt blev oprettet til målkanalen, eller om en eksisterende blev fundet og linket. Det er sikkert at kalde dette en anden gang — det returnerer den samme `contact_id` med `created: false` i stedet for at oprette en dublet.

En `422` betyder, at kontoen ikke kan udføre dette link lige nu: kontakten er allerede på den kanalfamilie, den har intet telefonnummer at bruge, eller der er ingen forbundet afsender til målkanalen. En `409` betyder, at de to kontakter allerede er linket til to forskellige personer — fjern linket for den ene først.

### Vis en kontakts linkede samtaler

`GET /contacts/{contactId}/linked`

Returnerer de andre samtaler, der er den samme person som denne kontakt. En ikke-linket kontakt returnerer et tomt array, ikke en `404` — "denne person har ingen andre kanaler" er en normal tilstand.

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/linked?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{
  "success": true,
  "data": [
    {
      "contact_id": "contact_def456",
      "channel": "sms",
      "custom_channel": null,
      "first_name": "Jane",
      "last_name": "Smith",
      "phone_number": "+15551234567",
      "last_message": "Sounds good, thanks!",
      "last_message_timestamp": "2026-06-09T10:21:00.000Z",
      "linked_from": {
        "contact_id": "contact_abc123",
        "channel": "whatsapp",
        "linked_at": "2026-06-01T09:00:00.000Z",
        "reason": "continue_on_channel"
      }
    }
  ]
}
```

### Fjern link for en kontakt

`DELETE /contacts/{contactId}/link`

Fjerner denne kontakt fra sin person, ensidigt — alle andre kontakter, der stadig er linket til den person, beholder deres link, så at fjerne linket for én ud af tre opløser ikke gruppen.

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/link?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{ "success": true }
```

---

## Hent en kontakts profilbillede

`POST /contacts/{contactId}/profile-pic`

Henter (og cacher) kontaktens WhatsApp- eller Meta-profilbillede efter behov — det samme billede, der returneres som `avatarUrl` i [Hent en kontakt](#get-a-contact-by-phone-or-email), opdateret.

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/profile-pic?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{
  "success": true,
  "avatar_url": "https://example.com/photo.jpg",
  "cached": false
}
```

`cached: true` betyder, at URL'en kom fra et nyligt opslag i stedet for et frisk udbyderopslag — billeder caches i 7 dage, og en kontakt, som udbyderen rapporterer ikke har et tilgængeligt billede, caches som utilgængelig i 24 timer. Når der ikke er noget billede at hente, udelades `avatar_url`, og `message` forklarer hvorfor.

---

## Auto-tag kontakter med AI

Kører din kontos tag-regler over en eller flere kontakters fulde samtaleliste og tilføjer (eller fjerner) tags præcis som den realtids-tagging, der kører under en live chat — samme regler, samme kreditomkostning pr. tag.

### Start en kørsel

`POST /contacts/auto-tag`

| Felt | Påkrævet | Beskrivelse |
|---|---|---|
| `scope` | Ja | `"contacts"` for at tagge specifikke kontakter, eller `"agent"` for at tagge alle samtaler, der i øjeblikket håndteres af én AI-agent. |
| `contact_ids` | Påkrævet når `scope` er `"contacts"` | Array af kontakt-ID'er, 1 til 500. |
| `agent_id` | Påkrævet når `scope` er `"agent"` | Den AI-agent, hvis samtaler der skal tagges. Når `scope` er `"contacts"`, er dette valgfrit og indsnævrer blot, hvilke af agentens tag-regler der køres. |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/auto-tag?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "contacts", "contact_ids": ["contact_abc123", "contact_def456"] }'
```

En **enkelt** kontakt køres inline og returnerer resultatet med det samme:

```json
{ "success": true, "result": { "tags_applied": 2, "tags_removed": 0 } }
```

**To eller flere** kontakter (eller `scope: "agent"`) køres som et baggrundsjob og returnerer `202` med det samme:

```json
{ "success": true, "run_id": "m1x2y3-a1b2c3d4", "total": 214 }
```

### Forespørg en kørsel

`GET /contacts/auto-tag/run`

Returnerer kontoens nuværende (eller seneste) kørsel, så du kan forespørge om status uden selv at skulle spore `run_id`.

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/auto-tag/run?apiKey=YOUR_API_KEY"
```

**Svar**

```json
{
  "success": true,
  "run": {
    "run_id": "m1x2y3-a1b2c3d4",
    "status": "running",
    "total": 214,
    "processed": 58,
    "tagged_contacts": 12,
    "tags_applied": 15,
    "tags_removed": 2,
    "credits_charged": 15
  }
}
```

`run` er `null`, når kontoen aldrig har startet en. `status` flytter sig fra `"running"` til `"completed"` eller `"failed"`.

Kun én bulk-kørsel kan være i gang pr. konto ad gangen — hvis du starter en anden, mens en anden kører, returneres `409` med `error_code: "auto_tag_run_in_progress"`. Hvis du løber tør for kreditter under en kørsel for en enkelt kontakt, returneres `402` med `error_code: "insufficient_credits"`; en bulk-kørsel stopper i stedet sig selv tidligt og rapporterer, hvor langt den nåede i `run`.

---

## Slet en kontakt

`DELETE /contacts/{contactId}`

Sletter permanent én kontakt via ID, sammen med dens beskedhistorik. **Dette kan ikke fortrydes.** For at slette flere kontakter i et enkelt kald, brug [Slet kontakter](#delete-contacts) herunder.

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  method: "DELETE",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
```

**Svar**

```json
{
  "success": true
}
```

Et kontakt-id, der ikke findes på din konto, eller som tilhører en anden konto, returnerer en `404`.

---

## Slet kontakter

`DELETE /contacts`

Sletter permanent en eller flere kontakter via id i et enkelt kald (op til 500 id'er). Id'er, der ikke findes på din konto, springes over og tælles i `skipped`. **Dette kan ikke fortrydes.**

| Felt | Beskrivelse |
|---|---|
| `contactIds` | Array af kontakt-id'er, der skal slettes (maks. 500). |

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactIds": ["contactId1", "contactId2"] }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts", {
  method: "DELETE",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ contactIds: ["contactId1", "contactId2"] }),
});
const data = await res.json();
console.log(`Deleted ${data.deleted}, skipped ${data.skipped}`);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"contactIds": ["contactId1", "contactId2"]},
)
data = res.json()
print(f"Deleted {data['deleted']}, skipped {data['skipped']}")
```

**Svar**

```json
{
  "success": true,
  "deleted": 2,
  "skipped": 0
}
```

---

## Slet et brugerdefineret felt

`DELETE /contacts/custom-fields/{fieldKey}`

Fjerner én brugerdefineret feltnøgle fra **alle** kontakter på din konto. Brug dette til at rydde op efter omdøbning eller fjernelse af et brugerdefineret felt. Nøglen må kun indeholde bogstaver, tal, understregninger og bindestreger. Returnerer hvor mange kontakter der blev opdateret. **Dette kan ikke fortrydes.**

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh", {
  method: "DELETE",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(`Removed from ${data.updated} contacts`);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(f"Removed from {res.json()['updated']} contacts")
```

**Svar**

```json
{
  "success": true,
  "updated": 42
}
```

::: note
**Bemærk:** En feltnøgle med ikke-understøttede tegn returnerer en `400`.
:::


---

## Lister

Lister grupperer kontakter. En liste er enten **statisk** (du bestemmer, hvem der er på den) eller **smart** (medlemskab beregnes ud fra regler og holdes automatisk opdateret — se [Organisering af lister og kontakter](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Felt | Beskrivelse |
|---|---|
| `name` | Påkrævet ved oprettelse. Op til 100 tegn. |
| `status` | `live` (standard) eller `draft`. Små bogstaver. |
| `contact_ids` | Array af kontakt-id'er, der skal tilføjes listen. **Kun statiske lister.** |
| `type` | `static` (standard) eller `smart`. |
| `smart_rules` | Regelsættet — påkrævet når `type` er `smart`. Se nedenfor. |

### Opret en liste

`POST /lists`

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Hot leads (active)",
        "type": "smart",
        "smart_rules": {
          "match": "all",
          "conditions": [
            { "field": "tags", "op": "has_any", "value": ["tagHotLead"] },
            { "field": "last_activity_at", "op": "within_last", "value": { "amount": 90, "unit": "days" } }
          ]
        }
      }'
```

**Svar**

```json
{
  "success": true,
  "list_id": "list_abc123",
  "evaluation": { "added": 3, "removed": 0, "total": 3 }
}
```

En smart liste evalueres **inline** i den samme anmodning, så `evaluation` fortæller dig præcis, hvem der endte på den. På en statisk liste er `evaluation` lig med `null`.

### Opdater en liste

`PUT /lists/{listId}`

Send kun de felter, du ændrer. Ændring af `smart_rules` gen-evaluerer listen med det samme og returnerer det samme `evaluation`-objekt.

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists/list_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "smart_rules": { "match": "any", "conditions": [ { "field": "tags", "op": "has_any", "value": ["tagHotLead", "tagWebinar"] } ] } }'
```

Du kan skifte en liste mellem de to typer:

- **Statisk → smart**: send `{ "type": "smart", "smart_rules": { … } }`. Reglerne tager over med det samme.
- **Smart → statisk**: send `{ "type": "static" }`. Reglerne fjernes, og alle, der er på listen, bliver der.

### Formen på `smart_rules`

```json
{
  "match": "all",
  "conditions": [
    { "field": "tags", "op": "has_any", "value": ["tagHotLead"] },
    { "field": "channel", "op": "is_any", "value": ["whatsapp", "sms"] },
    { "field": "last_incoming_message_at", "op": "not_within_last", "value": { "amount": 7, "unit": "days" } },
    { "field": "created_at", "op": "after", "value": "2026-01-01" },
    { "field": "is_bot_active", "op": "is", "value": true },
    { "field": "email", "op": "is_set" },
    { "field": "custom_field", "key": "Plan", "op": "eq", "value": "pro" }
  ]
}
```

- `match` — `all` (alle betingelser skal være sande) eller `any` (mindst én).
- `conditions` — 1 til 20 betingelser, hver med højst 100 værdier, strenge på op til 200 tegn.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | array af tag-ID'er |
| `lists` | `in_any`, `not_in_any` | array af liste-ID'er (**kun statiske lister** — en smart liste kan ikke bygges ud fra en anden smart liste) |
| `channel` | `is_any`, `is_none` | array af kanaler |
| `status` | `is_any`, `is_none` | array af kontaktstatusser |
| `created_at`, `last_activity_at`, `last_incoming_message_at`, `last_outgoing_message_at`, `first_ai_interaction_at`, `last_ai_interaction_at` | `within_last`, `not_within_last` | `{ "amount": 1–3650, "unit": "hours" \| "days" }` |
| samme dato-felter | `before`, `after` | ISO-dato (`"2026-01-01"`, sammenlignes som hele dage) eller fuld ISO-dato-tid (`"2026-01-01T14:30:00Z"`, sammenlignes med det præcise tidspunkt) |
| samme dato-felter | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` matcher kontakter, som AI'en har sendt mindst én besked til (nogensinde) |
| `is_bot_active`, `do_not_disturb`, `is_private`, `has_ever_responded` | `is` | `true` / `false` |
| `email`, `phone_number`, `first_name`, `last_name` | `is_set`, `not_set`, `contains`, `not_contains` | streng til `contains`-formularerne |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | array af ID'er til `is_any` / `is_none`-formularerne |
| `custom_field` (plus en `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | streng til værdiformularerne |

`not_within_last` matcher også kontakter, hvor datoen aldrig er blevet sat ("mere end N siden, **eller aldrig**"), og tekstsammenligninger ignorerer store/små bogstaver.

**AI-engagement.** `has_interacted_with_ai` er lifetime-flaget: `true` for enhver kontakt, som din AI har sendt mindst én besked til, `false` for alle andre (inklusive kontakter, som kun dit team nogensinde har svaret). Det stemples ved AI'ens første besked til en kontakt og slettes aldrig, så at slå AI-svar fra for kontakten eller flytte dem til en anden kampagne nulstiller det ikke. For en *periode* — "de kontakter min AI håndterede i denne måned", det sædvanlige faktureringsspørgsmål — skal du bruge et interval over `last_ai_interaction_at` i stedet:

```json
{ "field": "last_ai_interaction_at", "op": "within_last", "value": { "amount": 30, "unit": "days" } }
```

Forveksl ikke nogen af dem med `is_bot_active` (AI'en har *tilladelse* til at svare, ikke at den har gjort det) eller `has_ever_responded` (kontakten skrev tilbage, til hvem som helst). De samme to stempler returneres på hver kontakt som `first_ai_interaction_at` / `last_ai_interaction_at`, og hele regelsættet fungerer også på `GET /contacts?rules=`, så du kan tælle matches uden at oprette en liste.

### Få vist et regelsæt

`POST /lists/preview`

Tæller og sampler de kontakter, som et regelsæt ville matche, uden at oprette eller ændre noget. Brug det til at kontrollere reglerne, før du gemmer dem.

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists/preview?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "smart_rules": { "match": "all", "conditions": [ { "field": "tags", "op": "has_any", "value": ["tagHotLead"] } ] } }'
```

**Svar**

```json
{
  "success": true,
  "count": 3,
  "sample": [
    {
      "id": "contact_abc123",
      "first_name": "Sofia",
      "last_name": "Martinez",
      "phone_number": "+31600000000",
      "email": "sofia@example.com",
      "channel": "whatsapp"
    }
  ]
}
```

`sample` indeholder op til 10 kontakter, sorteret efter seneste aktivitet først.

### Kør en smart liste igen nu

`POST /lists/{listId}/evaluate`

Tvinger en øjeblikkelig genberegning (det samme som **Opdater nu** gør i dashboardet). Smarte lister opdateres allerede, når en kontakt ændres, og hvert 15. minut for tidsbaserede regler, så dette er kun nødvendigt, når du vil have resultatet *lige nu*.

**Svar**

```json
{
  "success": true,
  "list_id": "list_abc123",
  "evaluation": { "added": 2, "removed": 1, "total": 4 }
}
```

`evaluation.skipped: true` betyder, at en anden evaluering af den samme liste allerede kørte, og dette kald gjorde intet.

### Smarte lister afviser manuelt tilføjede medlemmer

Medlemskabs-endpoints returnerer **`409`** med `"This is a smart list — its members are computed from its rules. Edit the rules instead."`, når mållisten er smart. Det dækker `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` på `POST /lists` og `PUT /lists/{listId}`, samt valg af en smart liste som mål for CSV-import. Ændr reglerne i stedet.

At kalde `POST /lists/{listId}/evaluate` på en **statisk** liste er også en `409` — den har ingen regler at køre.

---

## Kontakter API-fejl

Kontakt-endpoints returnerer standardfejlkonvolutten:

```json
{
  "success": false,
  "error": "Contact not found"
}
```

Nogle slutpunkter inkluderer også `error_code`, som normalt matcher HTTP-status — den eneste undtagelse er tilfældet med dublerede kontakter nedenfor, hvor HTTP-status er `200`, og kun `error_code` bærer `409`. Koderne, der er specifikke for kontaktslutpunkter:

| Kode | Hvornår det sker på et kontakt-endepunkt |
|---|---|
| `400` | Ugyldig anmodning — et manglende/ugyldigt felt, tom brødtekst, dårlig markør eller over 500 id'er i en batch. |
| `402` | Ikke nok kreditter til at fuldføre en AI-tagging-kørsel på én kontakt (`error_code: "insufficient_credits"`). |
| `404` | Kontakten, listen eller tagget blev ikke fundet på din konto. |
| `409` | En kontakt med det telefonnummer findes allerede (ved oprettelse). Returneres som `error_code` i brødteksten med en HTTP-status på `200`, så forgrening på `error_code` her. Returneres også, når en automatisk bulk-tagging-kørsel allerede er i gang (`error_code: "auto_tag_run_in_progress"`), eller når sammenkædning af en kontakt til en anden kanal ville forbinde to kontakter, der allerede er knyttet til to forskellige personer. |
| `422` | Kontakten kan ikke modtage en besked lige nu (forstyr ikke, privat eller ikke-understøttet kanal). På kanal-link-endepunktet dækker det også intet telefonnummer, en ikke-understøttet kanalparring eller ingen forbundet afsender for målkanalen. |

En `403` på et kontaktslutpunkt kan også betyde et problem med kontaktgrænse eller listetilladelse frem for abonnementsadgang. De delte koder, som ethvert slutpunkt kan returnere — `401`, `403` (dit abonnement inkluderer ikke API-adgang), `429` (rate limit) og `500` — er angivet med vejledning til genforsøg i [Fejl & Sidetal](errors-and-pagination.md).

---

## Næste skridt

- [Besked-API](messages.md) — send beskeder via kanalidentitet og administrer samtaler.
- [API-reference](reference.md) — fuld liste over endpoints, inklusive tags og lister.
- [API-adgang](../integrations/api-access.md) — godkendelse, hastighedsbegrænsninger og fejlhåndtering.
