HelipodDocs

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 }).

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

GET/contacts
Page through contacts, ordered by id.
QueryTypeNotes
statussubscribed | unsubscribed | bouncedFilter by status.
searchstringMatches email, first name or last name (contains). Up to 200 characters.
segmentIdstringOnly contacts in this segment.
topicIdstringOnly contacts subscribed to this topic.
limitnumber1 to 200, default 50.
cursorstringnextCursor 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

POST/contacts
Add one contact or up to 1,000. Existing emails are updated (upsert on email).

The 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

GET/contacts/{id}
One contact with its properties and topic subscriptions.
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

PATCH/contacts/{id}
Change names, status or properties.

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

DELETE/contacts/{id}
Delete a contact.
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

PUT/contacts/{id}/properties
Set custom properties. Existing keys are overwritten, other keys are kept.

Prop

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

PUT/contacts/{id}/topics
Subscribe the contact to the given topics.

Prop

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

POST/contacts/{id}/unsubscribe
Opt a contact out of everything, or out of one topic.

Prop

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 unsubscribed and the address is added to the suppression list with reason unsubscribed. 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 subscribed for 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

POST/contacts/import
Import contacts from CSV text. The import runs in the background.

Prop

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; rejected counts them and rejectedSample lists 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

GET/contacts/export
Export up to 50,000 contacts as CSV.
curl 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.

On this page