Notideus
API Reference

Contacts

Create, read, update, delete, bulk-upsert and unsubscribe 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

FieldTypeNotes
iduuidStable contact identifier
emailstringUnique per team; the upsert key
statusstringOne of subscribed, unsubscribed, bounced, complained — see Contact statuses
propertiesmapstringstringFree-form key/value data; empty as {}, never null
subscribed_topic_idsuuidTopics the contact opted into
created_attimestampRFC 3339
updated_attimestampRFC 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

FieldTypeRequiredNotes
emailstringyesUnique per team
propertiesmapstringstringnoFree-form key/value data
topic_idsuuidnoTopics 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

ParameterTypeNotes
emailstringThe 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

ParameterTypeRequiredNotes
limitintegernoPage size; default 20, max 100
cursorstringnoInteger offset as a string; taken from next_cursor of the previous page
emailstringnoSubstring match, case-insensitive
statusstringnoFilter 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

ParameterTypeNotes
emailstringThe contact's email address

Request body

FieldTypeRequiredNotes
propertiesmapstringstringnoReplaces the whole map — omitted keys are deleted
topic_idsuuidnoReplaces 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

ParameterTypeNotes
emailstringThe 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).

Bulk upsert never resets 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

ParameterTypeNotes
emailstringThe 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

ParameterTypeNotes
emailstringThe 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

FieldTypeRequiredNotes
emailstringyesThe contact's email address
topic_idsuuidnoUnsubscribe 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).

Copyright © 2026