Docs/API reference/Contacts

Contacts

Programmatic audience management. All endpoints are workspace-scoped through your API key, and emails are trimmed and lowercased before matching.

v1 · beta

Endpoints

EndpointMethodPurpose
/api/v1/contactsPOSTUpsert a contact by email
/api/v1/contacts?email=…GETFetch a contact with its tags
/api/v1/contacts/updatePOSTUpdate an existing contact (404 if missing)
/api/v1/contacts/deletePOSTDelete a contact (idempotent)
/api/v1/contacts/tags/addPOSTAttach a tag (auto-created, idempotent)
/api/v1/contacts/tags/removePOSTDetach a tag (idempotent)
/api/v1/contacts/unsubscribePOSTOpt an address out of a topic, or of all marketing
/api/v1/contacts/resubscribePOSTRemove an opt-out

Upsert a contact

bash
curl -X POST https://www.ewyn.ai/api/v1/contacts \
  -H "Authorization: Bearer $EWYN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "firstName": "Ada",
    "tags": ["customer", "beta"]
  }'

Body fields:

FieldTypeRequiredNotes
emailstringyesTrimmed and lowercased server-side
firstNamestringnoOverwrites the stored first name when provided
lastNamestringnoOverwrites the stored last name when provided
tagsstring[]noTag names to add — auto-created if missing; max 50 per call, 100 chars each

Response:

json
{
  "contact": {
    "id": "ct_...",
    "email": "user@example.com",
    "firstName": "Ada",
    "lastName": null,
    "tags": [{ "id": "tag_...", "name": "customer", "color": "#..." }]
  },
  "created": true,
  "suppressed": false,
  "optOuts": [
    { "topic": "newsletter", "createdAt": "2026-10-20T08:15:00.000Z" }
  ]
}

suppressed is a delivery block: the address hard-bounced, complained, or was suppressed by hand. optOuts lists the marketing opt-outs — one entry per topic slug, or topic: null for all marketing. The same two fields come back from GET /api/v1/contacts and /contacts/update.

Tag operations

bash
# attach
curl -X POST https://www.ewyn.ai/api/v1/contacts/tags/add \
  -H "Authorization: Bearer $EWYN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com", "tag": "vip" }'

# detach
curl -X POST https://www.ewyn.ai/api/v1/contacts/tags/remove \
  -H "Authorization: Bearer $EWYN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com", "tag": "beta" }'

tags/add returns the attached tag; tags/remove returns removed: false instead of failing when the tag wasn't attached. Both return 404 when the contact itself doesn't exist.

Unsubscribe and resubscribe

Both take { email, topic? }. topic is a topic slug from Audience → Topics; omit it to stop, or resume, all marketing email. Neither endpoint needs a contact record — opt-outs are kept per address.

bash
# stop the newsletter
curl -X POST https://www.ewyn.ai/api/v1/contacts/unsubscribe \
  -H "Authorization: Bearer $EWYN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com", "topic": "newsletter" }'

# resume all marketing email
curl -X POST https://www.ewyn.ai/api/v1/contacts/resubscribe \
  -H "Authorization: Bearer $EWYN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com" }'

Responses:

json
// unsubscribe
{ "unsubscribed": true, "created": true, "optOuts": [{ "topic": "newsletter", "createdAt": "..." }] }

// resubscribe
{ "resubscribed": true, "removed": true, "optOuts": [] }

created: false and removed: false mean the opt-out already existed, or didn't — retries are safe. A topic that names no active topic returns 404 with code TOPIC_NOT_FOUND. Resubscribing from all marketing removes only the all-marketing opt-out; per-topic opt-outs stay until they are removed one by one.

Semantics

  • Merge, never erase — upserts only overwrite the fields you provide; tags merge additively
  • Idempotent by design — deletes and tag-removals report deleted/removed: false rather than erroring, so retries are always safe
  • Tag names are case-sensitive — VIP and vip are different tags
Opt-outs stop marketing; suppressions stop delivery.

An opt-out blocks marketing send requests under that topic (or all of them) with 422 and never touches transactional email. suppressed: true blocks every send after a hard bounce or a manual suppression, and marketing sends after a spam complaint; there is no API to remove a suppression — that's deliberate. Only resubscribe someone who asked you to: the unsubscribe page cannot do it for them. The rules are spelled out in Subscriptions and unsubscribe.

Using TypeScript? The SDK contact methods wrap these endpoints one-to-one.

Was this page helpful?