Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
| /api/v1/contacts | POST | Upsert a contact by email |
| /api/v1/contacts?email=… | GET | Fetch a contact with its tags |
| /api/v1/contacts/update | POST | Update an existing contact (404 if missing) |
| /api/v1/contacts/delete | POST | Delete a contact (idempotent) |
| /api/v1/contacts/tags/add | POST | Attach a tag (auto-created, idempotent) |
| /api/v1/contacts/tags/remove | POST | Detach a tag (idempotent) |
| /api/v1/contacts/unsubscribe | POST | Opt an address out of a topic, or of all marketing |
| /api/v1/contacts/resubscribe | POST | Remove an opt-out |
Upsert a contact
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:
| Field | Type | Required | Notes |
|---|---|---|---|
string | yes | Trimmed and lowercased server-side | |
| firstName | string | no | Overwrites the stored first name when provided |
| lastName | string | no | Overwrites the stored last name when provided |
| tags | string[] | no | Tag names to add — auto-created if missing; max 50 per call, 100 chars each |
Response:
{
"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
# 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.
# 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:
// 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: falserather than erroring, so retries are always safe - Tag names are case-sensitive —
VIPandvipare different tags
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.