---
title: "Create or update a contact"
method: POST
path: "/contacts"
tags: ["Contacts"]
---

# Create or update a contact

`POST /contacts`

## Request

- Base URL: `https://sending.dev/api/v1`
- URL: `https://sending.dev/api/v1/contacts`
- Auth: HTTP bearer

Creates the contact with its per-channel identities, or attaches to the one that already has one of those identities (the per-channel address is the natural key). Idempotent: repeating the same request answers 200 with `created: false` instead of failing. `attributes` are merged with the existing ones (`null` on a key deletes it) and `consent` is touched only if declared. 409 if the request's identities belong to different contacts: merging two contacts is irreversible and is not done automatically. On a NEW email identity, `warnings` flags a suspicious domain typo or an address already undeliverable on SES (suppressed immediately for this tenant): absent, never an empty array, when there is nothing to say. Required scope: `contacts:write`.

## Request body

- CreateContact
  - `attributes` object
  - `identities` object[]
    - `channel` 'email' | 'whatsapp' | 'telegram', required
    - `address` string, required
    - `consent` 'unknown' | 'opted_in' | 'opted_out' | 'not_subscribed'

## Response `200`

Contact already existed, updated.

- ContactUpserted
  - `id` string, uuid, required
  - `created` boolean, required
  - `identitiesAdded` integer, required
  - `warnings` string[]

## Other responses

- `201`: Contact created.
- `401`: Missing or invalid token.
- `402`: Plan quota exceeded.
- `403`: Insufficient scope.
- `409`: State conflict (e.g. duplicate).
- `422`: Invalid input.

---

[API](https://skmtc.dev/sending/apis/sending-dev-api.md) · [All operations](https://skmtc.dev/sending/apis/sending-dev-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sending/apis/sending-dev-api/revisions/9fe78d2918a1?raw)
