curl --request DELETE \
--url https://api.flow.engineer/v0/contacts/{contact_id} \
--header 'Authorization: Bearer <token>'const options = {method: 'DELETE', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.flow.engineer/v0/contacts/{contact_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v0/contacts/{contact_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.delete(url, headers=headers)
print(response.text)package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v0/contacts/{contact_id}"
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "authentication",
"message": "No valid API key was given.",
"hint": "Send the header Authorization: Bearer fk_test_... (or fk_live_...); no key yet? Get a test key with curl -X POST https://api.flow.engineer/v0/sandbox/keys",
"doc_url": "https://api.flow.engineer/docs/errors/authentication"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v0/conversations/{conversation_id}/template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"channel": "telegram",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v0/conversations/{conversation_id}/template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"channel": "telegram",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Delete a contact and their data
Erases the contact and what Flow keeps about them in this app and mode. It cannot be undone. Use it when the person asks you to delete their data, or when you delete them in your own product.
Erased at once:
- the contact’s name and address (phone, username, Telegram user ID, iMessage handle) and, on SMS, their recorded consent and opt-out;
- the content of every message in their conversations, inbound and outbound, with reply quotes, metadata and what the channel reported back;
- the files sent or received in those conversations, including uploads you
sent them (a
file_idsent to a deleted contact is not found afterwards); - the events of those conversations: they are no longer returned by
GET /v0/events, the stream or webhooks (an event ID you hold still works as anafterorbeforecursor); - webhook deliveries of those events still pending (they are not sent), and messages still queued to the contact (they are not sent; a message already handed to the channel may still arrive);
- saved
Idempotency-Keyanswers that held their data: repeating such a request answers404 not_found.
Kept: IDs, timestamps, and the counts that bill and pace your senders
(allowances, new-contact budgets, warm-up). The contact and their
conversations answer 404 not_found from then on and are not listed. Flow
cannot delete what the channel itself keeps, such as the chat on the
person’s own device.
On a shared sandbox sender the person leaves your app and must send the join
message again to talk to it. On SMS the opt-out is erased with the contact:
carriers keep blocking a number that texted STOP, so do not text it again
unless the person opts in again. If the person writes again, or you send to their address, they are a
new contact with a new ID and a new conversation (conversation.started),
and the channel’s usual rules apply, such as the WhatsApp 24-hour window.
The first delete emits contact.deleted, which carries only the contact’s ID.
Deleting a contact that is already deleted also answers 204, so the call is
safe to retry. An ID this app and mode never had (including a contact of your
other mode) answers 404 not_found with param contact_id, as GET does.
curl --request DELETE \
--url https://api.flow.engineer/v0/contacts/{contact_id} \
--header 'Authorization: Bearer <token>'const options = {method: 'DELETE', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.flow.engineer/v0/contacts/{contact_id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.flow.engineer/v0/contacts/{contact_id}"
headers = {"Authorization": "Bearer <token>"}
response = requests.delete(url, headers=headers)
print(response.text)package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.flow.engineer/v0/contacts/{contact_id}"
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"error": {
"type": "invalid_request",
"message": "limit must be between 1 and 100.",
"hint": "Pass limit between 1 and 100 (default 20), and page with after or before.",
"doc_url": "https://api.flow.engineer/docs/errors/invalid_request",
"param": "limit"
}
}{
"error": {
"type": "authentication",
"message": "No valid API key was given.",
"hint": "Send the header Authorization: Bearer fk_test_... (or fk_live_...); no key yet? Get a test key with curl -X POST https://api.flow.engineer/v0/sandbox/keys",
"doc_url": "https://api.flow.engineer/docs/errors/authentication"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v0/conversations/{conversation_id}/template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"channel": "telegram",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}{
"error": {
"type": "new_contact_limit",
"message": "This sender has started its 15 new conversations for today.",
"hint": "Retry after 3600 seconds; replies into existing conversations still go.",
"doc_url": "https://api.flow.engineer/docs/errors/new_contact_limit",
"retry_after": 3600,
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T"
}
}{
"error": {
"type": "invalid_request",
"message": "<string>",
"hint": "Send a template instead: POST /v0/conversations/{conversation_id}/template.",
"doc_url": "https://api.flow.engineer/docs/errors/outside_window",
"param": "<string>",
"retry_after": 1,
"channel": "telegram",
"conversation": "conv_01JB8ZC3K5M7P9R1T3V5X7Z9B1",
"sender": "snd_01JB8Z4Q3V6W0R2N7C5H1M9K4T",
"channel_code": "<string>",
"request_id": "<string>"
}
}Authorizations
An API key of one app, sent as Authorization: Bearer <key>. Keys start with
fk_test_ (test mode: sandbox senders and test data only) or fk_live_
(live mode). Keep live keys on your server; never ship them in an app or page.
Headers
The API version to use, as a date. Without it, the version pinned to your app when it was created is used.
"2026-11-01"
Path Parameters
The contact's ID.
A contact ID, ct_ and a ULID.
^ct_[0-9A-HJKMNP-TV-Z]{26}$"ct_01JB8ZB2J4K6N8Q0S2V4W6Y8A0"
Response
The contact is deleted (or was already).