Contacts
Store the people you send broadcasts to, with custom properties, topic subscriptions and CSV import/export.
Contacts are your marketing audience: broadcasts go to contacts, filtered by segments and topics. Transactional sends with POST /send do not need a contact.
All endpoints on this page accept an email API key (or a Helipod API key with the Email API enabled) and act on the key's project.
Contact object
Prop
Type
GET /contacts/{id} also returns properties (an object of custom properties) and topics (an array of { id, name, subscribed }).
Consent
Only add people who agreed to receive email from you. Purchased or scraped lists cause the bounce and complaint spikes that pause sending for a project. Bulk adds and imports must confirm consent with consentConfirmed: true; the consent source and time are recorded on each contact.
List contacts
/contacts| Query | Type | Notes |
|---|---|---|
status | subscribed | unsubscribed | bounced | Filter by status. |
search | string | Matches email, first name or last name (contains). Up to 200 characters. |
segmentId | string | Only contacts in this segment. |
topicId | string | Only contacts subscribed to this topic. |
limit | number | 1 to 200, default 50. |
cursor | string | nextCursor from the previous page (a contact id). |
curl "https://mail.helipod.io/api/email/contacts?status=subscribed&limit=100" \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"data": [
{
"id": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"email": "[email protected]",
"firstName": "Ana",
"lastName": "Putri",
"status": "subscribed",
"subscribedAt": "2026-10-01T08:00:00.000Z",
"unsubscribedAt": null,
"bounceCount": 0,
"createdAt": "2026-10-01T08:00:00.000Z"
}
],
"nextCursor": null
}
}segmentId and topicId are applied after a page is read, so a page can hold fewer than limit rows (even zero) while nextCursor is not null. Keep paging until nextCursor is null. When segmentId is used, the response also carries note: "segment filter applied after pagination".
Add contacts
/contactsThe body takes one of three shapes.
One contact:
Prop
Type
curl -X POST https://mail.helipod.io/api/email/contacts \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"firstName": "Ana",
"lastName": "Putri",
"properties": { "plan": "pro", "city": "Bandung" },
"consentSource": "signup_form"
}'Many contacts: wrap them in contacts and confirm consent.
Prop
Type
curl -X POST https://mail.helipod.io/api/email/contacts \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"consentConfirmed": true,
"consentSource": "newsletter_form",
"contacts": [
{ "email": "[email protected]", "firstName": "Ana" },
{ "email": "[email protected]", "firstName": "Budi" }
]
}'Plain array (accepted for older integrations): a JSON array of contacts. Consent is recorded only if you add ?consentConfirmed=true (and optionally &consentSource=...) to the URL; otherwise the contacts are stored as unconfirmed.
Response 201: data is always an array of contact objects, even for a single contact.
Checks run on every address before anything is written. If any address fails, nothing is added:
400 INVALID_EMAIL: not a valid address.400 DISPOSABLE_EMAIL: a disposable (throwaway) inbox provider.400 RECIPIENT_DOMAIN_UNDELIVERABLE: the domain cannot receive mail.403 RECIPIENT_SUPPRESSED: on the project's suppression list.402 CONTACT_LIMIT_REACHED: the request would take the project over the plan's contact limit (every entry in the request counts, including ones that already exist).400 EMPTY_REQUEST,400 TOO_MANY: no contacts, or more than 1,000.
Re-adding an existing email updates it. firstName and lastName are overwritten with what you send, so omitting them clears them. Status and recorded consent are kept.
Get a contact
/contacts/{id}curl https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"id": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"email": "[email protected]",
"firstName": "Ana",
"lastName": "Putri",
"status": "subscribed",
"subscribedAt": "2026-10-01T08:00:00.000Z",
"unsubscribedAt": null,
"bounceCount": 0,
"createdAt": "2026-10-01T08:00:00.000Z",
"properties": { "plan": "pro", "city": "Bandung" },
"topics": [{ "id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d", "name": "product-updates", "subscribed": true }]
}
}Returns 404 CONTACT_NOT_FOUND for an unknown id.
Update a contact
/contacts/{id}Prop
Type
curl -X PATCH https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{"lastName":"Wijaya","properties":{"plan":"business"}}'Returns the contact object (without properties and topics).
Delete a contact
/contacts/{id}curl -X DELETE https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e \
-H "Authorization: Bearer your_email_api_key"{ "success": true, "data": { "deleted": true } }Deleting a contact does not add it to the suppression list. To make sure an address is never mailed again, unsubscribe it or add a suppression.
Set properties
/contacts/{id}/propertiesProp
Type
curl -X PUT https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e/properties \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{"properties":{"plan":"business","company":"Toko Ana"}}'{ "success": true, "data": { "count": 2 } }Properties can be used in segment filters and as {{tokens}} in broadcasts.
Subscribe to topics
/contacts/{id}/topicsProp
Type
curl -X PUT https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e/topics \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{"topicIds":["6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d"]}'{ "success": true, "data": { "topics": 1 } }This only adds or re-enables subscriptions. Topics not in the list are left as they are. To leave a topic, use unsubscribe with the topic name.
Unsubscribe a contact
/contacts/{id}/unsubscribeProp
Type
curl -X POST https://mail.helipod.io/api/email/contacts/0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e/unsubscribe \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{"topic":"product-updates"}'- Everything: the contact becomes
unsubscribedand the address is added to the suppression list with reasonunsubscribed. Response:{ "status": "unsubscribed", "suppressed": true }. This blocks broadcasts, not transactional sends. - One topic: the contact's subscription to that topic is turned off; the contact stays
subscribedfor everything else. Response:{ "status": "unsubscribed", "suppressed": false }.
Recipients can also unsubscribe themselves with the link in every broadcast and the unsubscribe header on every email.
Import from CSV
/contacts/importProp
Type
curl -X POST https://mail.helipod.io/api/email/contacts/import \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"consentConfirmed": true,
"consentSource": "event_signup",
"csv": "email,first_name,last_name\[email protected],Ana,Putri\[email protected],Budi,Santoso"
}'{
"success": true,
"data": {
"jobId": "eml_2b3c4d5e6f7a8b9c0d1e",
"parsed": 2,
"rejected": 0,
"rejectedSample": []
}
}- Only the first 5,000 data rows of the file are read. Split larger lists into several imports.
- Rows with a disposable or undeliverable domain are dropped before the import starts;
rejectedcounts them andrejectedSamplelists up to 20 with the reason. - Header names are case-insensitive. Values may be quoted with
". - Errors:
400 EMPTY_CSV,400 CSV_MISSING_EMAIL,400 NO_VALID_CONTACTS(every row was dropped),402 CONTACT_LIMIT_REACHED.
Poll the jobId; when it completes, its result reports how many contacts were created and skipped.
Export to CSV
/contacts/exportcurl https://mail.helipod.io/api/email/contacts/export \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"csv": "email,first_name,last_name,status,subscribed_at\n[email protected],Ana,Putri,subscribed,2026-10-01T08:00:00.000Z\n",
"filename": "contacts.csv"
}
}Rows are sorted by email.