Contacts
Base URL: https://api.notideus.io. Contacts are keyed by email address and carry a free-form properties map. Failures use the standard error envelope: Errors.
The contact model
| Field | Type | Notes |
|---|---|---|
id | uuid | Stable contact identifier |
email | string | Unique per team; the upsert key |
status | string | One of subscribed, unsubscribed, bounced, complained — see Contact statuses |
properties | mapstringstring | Free-form key/value data; empty as {}, never null |
subscribed_topic_ids | uuid | Topics the contact opted into |
created_at | timestamp | RFC 3339 |
updated_at | timestamp | RFC 3339 |
bounced and complained are deliverability suppressions set by SES events, not by the API — they block resubscription (409 contact_suppressed).
POST /v1/contacts
Scope: contacts:write — API key only.
Creates one contact.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Unique per team |
properties | mapstringstring | no | Free-form key/value data |
topic_ids | uuid | no | Topics to subscribe the contact to |
Request example
curl -X POST https://api.notideus.io/v1/contacts \
-H "Authorization: Bearer $NT_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"jane@example.com","properties":{"plan":"pro","signup":"2026-09-14"}}'
Response
201 Created — the new contact:
{
"id": "01a0a11c-81b6-7531-961e-c5e5bdacf7a6",
"email": "jane@example.com",
"status": "subscribed",
"properties": {
"plan": "pro",
"signup": "2026-09-14"
},
"subscribed_topic_ids": [],
"created_at": "2026-09-14T18:09:55.124431Z",
"updated_at": "2026-09-14T18:09:55.124431Z"
}
New contacts start as subscribed. A duplicate email is rejected — 409 Conflict with contact_exists, nothing is overwritten:
{
"error": {
"code": "contact_exists",
"message": "contact already exists"
}
}
GET /v1/contacts/:email
Scope: contacts:read — API key only.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
email | string | The contact's email address (URL-encoded if needed) |
Response
200 OK — the contact. 404 Not Found — no contact with that email:
{
"error": {
"code": "contact_not_found",
"message": "contact not found"
}
}
GET /v1/contacts
Scope: contacts:read — API key only.
Lists contacts, newest first, one page at a time.
Query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
limit | integer | no | Page size; default 20, max 100 |
cursor | string | no | Integer offset as a string; taken from next_cursor of the previous page |
email | string | no | Substring match, case-insensitive |
status | string | no | Filter by one of subscribed, unsubscribed, bounced, complained |
Response
200 OK — data holds the page; next_cursor is present while more pages exist. A real first page with limit=1 (note next_cursor is the offset string "1"):
{
"data": [
{
"id": "01a0a11c-81b6-7531-961e-c5e5bdacf7a6",
"email": "jane@example.com",
"status": "subscribed",
"properties": {
"plan": "pro",
"signup": "2026-09-14"
},
"subscribed_topic_ids": [],
"created_at": "2026-09-14T18:09:55.124431Z",
"updated_at": "2026-09-14T18:09:55.124431Z"
}
],
"next_cursor": "1"
}
Pass it back as ?cursor=1 for the next page. The final page has no next_cursor and can be as small as {"data":[]}.
PATCH /v1/contacts/:email
Scope: contacts:write — API key only.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
email | string | The contact's email address |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
properties | mapstringstring | no | Replaces the whole map — omitted keys are deleted |
topic_ids | uuid | no | Replaces the subscription list |
Response
200 OK — the updated contact. Replace semantics throughout: sending {"properties":{"plan":"enterprise"}} drops every other key. A real response:
{
"id": "01a0a11c-81b6-7531-961e-c5e5bdacf7a6",
"email": "jane@example.com",
"status": "subscribed",
"properties": {
"plan": "enterprise"
},
"subscribed_topic_ids": [],
"created_at": "2026-09-14T18:09:55.124431Z",
"updated_at": "2026-09-14T18:10:02.97923Z"
}
404 Not Found — no contact with that email.
DELETE /v1/contacts/:email
Scope: contacts:write — API key only.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
email | string | The contact's email address |
Response
204 No Content — the contact was deleted; the body is empty. 404 Not Found — no contact with that email.
POST /v1/contacts/bulk
Scope: contacts:write — API key only.
Upserts up to 1000 contacts in one call, keyed on email. The body is a contacts array; each item takes only email and properties (no topic_ids):
{
"contacts": [
{ "email": "jane@example.com", "properties": { "plan": "pro" } },
{ "email": "john@example.com", "properties": { "plan": "free" } },
{ "email": "not-an-email", "properties": {} }
]
}
Response
200 OK — one indexed result per item. Existing contacts return their existing id (upsert, not create-or-fail); invalid items fail individually:
{
"data": [
{
"index": 0,
"id": "01a0a11c-81b6-7531-961e-c5e5bdacf7a6"
},
{
"index": 1,
"id": "01a0a11c-c703-7e67-ae8c-ecb86e191395"
},
{
"index": 2,
"error": {
"code": "bad_request",
"message": "invalid email address"
}
}
]
}
Whole-call failures: 400 batch_too_large (more than 1000 items), 402 quota_exceeded (contact allowance reached — see Quotas and limits).
status — re-running a sync cannot accidentally resubscribe an unsubscribed, bounced or complained contact. Only properties are written.POST /v1/contacts/:email/unsubscribe
Scope: contacts:write — API key only.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
email | string | The contact's email address |
Response
200 OK — the contact with status now unsubscribed. A real response:
{
"id": "01a0a11c-81b6-7531-961e-c5e5bdacf7a6",
"email": "jane@example.com",
"status": "unsubscribed",
"properties": {
"plan": "pro"
},
"subscribed_topic_ids": [],
"created_at": "2026-09-14T18:09:55.124431Z",
"updated_at": "2026-09-14T18:10:12.884793Z"
}
The endpoint is idempotent: calling it again on an already-unsubscribed contact is a no-op returning the same shape. 404 Not Found — no contact with that email.
POST /v1/contacts/:email/resubscribe
Scope: contacts:write — API key only.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
email | string | The contact's email address |
Response
200 OK — the contact with status back to subscribed. 409 Conflict with contact_suppressed when the contact is bounced or complained — deliverability suppression cannot be lifted through the API. 404 Not Found — no contact with that email.
POST /v1/marketing/unsubscribe
Scope: none — dual-auth: accepts an API key or a session token.
Programmatic one-shot unsubscribe for support tooling: unsubscribes an arbitrary email in one call, without fetching the contact first.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | The contact's email address |
topic_ids | uuid | no | Unsubscribe from specific topics only |
Request example
curl -X POST https://api.notideus.io/v1/marketing/unsubscribe \
-H "Authorization: Bearer $NT_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"jane@example.com"}'
Response
204 No Content — no response body. Errors: 400 bad_request (invalid email), 404 contact_not_found (no contact with that email).