Contacts, segments and topics
Before you send a broadcast, you need people to send it to. Ha Tang organizes recipients the way Resend does today:
- A contact is one email address in your business (case does not matter), with a first and last name, custom properties and an "unsubscribed from everything" flag.
- A segment is a fixed group of contacts you send broadcasts to. A contact can be in many segments; deleting a segment keeps its contacts. Segments used to be called audiences, and the
/audiencesAPI still works. - A topic is a kind of email, such as promotions or a weekly newsletter, that each recipient can subscribe to or leave on the preferences page.
In the dashboard, everything is under Email → Audiences, in the Contacts, Segments, Topics, Properties and Suppressions tabs.
Before you start
The contact, segment, topic and property endpoints need a key with email:write.
Add contacts
- cURL
- Node.js
- Python
- PHP
curl -X POST https://api.hatang.dev/contacts \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Lan",
"last_name": "Tran",
"properties": { "plan": "pro" },
"segments": [{ "id": "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10" }],
"topics": [{ "id": "topic_0199e6c17a2c7c418f0e61f1b2a3c4d5", "subscription": "opt_in" }]
}'
# { "object": "contact", "id": "con_…" }
const { data, error } = await hatang.contacts.create({
first_name: "Lan",
last_name: "Tran",
properties: { plan: "pro" },
segments: [{ id: "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10" }],
topics: [{ id: "topic_0199e6c17a2c7c418f0e61f1b2a3c4d5", subscription: "opt_in" }],
});
requests.post(
"https://api.hatang.dev/contacts",
headers={"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}"},
json={
"first_name": "Lan",
"properties": {"plan": "pro"},
"segments": [{"id": "aud_0199e6c17a2b7f009c1e3f6a2b1d9e10"}],
},
)
$client->post('/contacts', [
'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
'json' => [
'first_name' => 'Lan',
'properties' => ['plan' => 'pro'],
'segments' => [['id' => 'aud_0199e6c17a2b7f009c1e3f6a2b1d9e10']],
],
]);
| Field | Description |
|---|---|
email | Required. Unique in your business. |
first_name, last_name | Up to 100 characters each. |
unsubscribed | true unsubscribes the contact from everything. |
properties | Values for declared properties. null removes a value. |
segments | Up to 50 { "id" } to add the contact to. |
topics | Up to 50 { "id", "subscription" } pairs, where subscription is opt_in or opt_out. |
POST /contacts creates the contact, or updates it when the address already exists, and returns its id. The contact.created, contact.updated and contact.deleted webhooks fire on changes. A new contact also starts automations triggered by hatang:contact.created; see Automations.
In the dashboard you can also Add contact one by one or Import CSV. The first row of the CSV is the header with any of these columns: email, first_name, last_name, unsubscribed and your property keys. Other columns are ignored and existing addresses are updated. Customers in the CRM can be synced too (Customers → Sync to Email); syncing never resubscribes someone who unsubscribed.
Custom properties
Declare a property before you set it on contacts. A property is a string or a number, with an optional fallback value:
curl -X POST https://api.hatang.dev/contact-properties \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{ "key": "plan", "type": "string", "fallback_value": "free" }'
- Keys use letters, digits and
_, up to 50 characters. String values are up to 2,000 characters. - Setting a property that is not declared returns
422. Anumberproperty refuses values that are not numbers. - Only the fallback can change later (
PATCH /contact-properties/{id}). Deleting a property removes its value from every contact.
Use properties in broadcasts and automation emails as {{{contact.plan}}}, next to {{{contact.first_name}}}, {{{contact.last_name}}} and {{{contact.email}}}. A contact without a value gets the inline default ({{{contact.plan|free}}}), then the property's fallback.
Segments
curl -X POST https://api.hatang.dev/segments \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Customers 2026" }'
# { "object": "segment", "id": "aud_…", "name": "Customers 2026", "created_at": "…" }
curl -X POST https://api.hatang.dev/contacts/[email protected]/segments/aud_0199e6c17a2b7f009c1e3f6a2b1d9e10 \
-H "Authorization: Bearer $HATANG_API_KEY"
Segment names are up to 100 characters. Segment ids start with aud_, because a segment is what used to be an audience.
Topics and the preferences page
curl -X POST https://api.hatang.dev/topics \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Promotions", "description": "Sales and discount codes", "default_subscription": "opt_in", "visibility": "public" }'
| Field | Description |
|---|---|
name | Up to 50 characters. |
description | Up to 200 characters, shown on the preferences page. |
default_subscription | opt_in: contacts get it until they leave. opt_out: contacts get it only after they subscribe. Fixed once created. |
visibility | public shows the topic to everyone on the preferences page. private (the default) shows it only to contacts subscribed to it. |
How topics filter mail:
- A broadcast with a
topic_idonly reaches contacts subscribed to that topic. POST /emailswith atopic_idskips recipients who left that topic; they are reported as suppressed. Addresses that are not contacts follow the topic's default.- A contact's choices are in
GET /contacts/{id}/topicsand change withPATCH /contacts/{id}/topics:
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '[{ "id": "topic_0199e6c17a2c7c418f0e61f1b2a3c4d5", "subscription": "opt_out" }]'
The {{{UNSUBSCRIBE_URL}}} link in a broadcast opens the preferences page, where the recipient switches each topic on or off or unsubscribes from everything. Opening the page changes nothing, so link scanners cannot unsubscribe anyone by accident. The page is in English when the browser prefers English and in Vietnamese otherwise. Set its title, description, logo and colors under Email settings → Unsubscribe page. How one-click unsubscribe works is in Broadcasts.
API reference
| Endpoint | Description |
|---|---|
POST /contacts, GET /contacts | Create or update a contact; list contacts (limit, after, segment_id, q). |
GET, PATCH, DELETE /contacts/{id or email} | One contact. |
GET /contacts/{id}/segments, POST or DELETE /contacts/{id}/segments/{segment_id} | Segment membership. |
GET, PATCH /contacts/{id}/topics | Topic choices. |
POST /segments, GET /segments, GET, PATCH, DELETE /segments/{id}, GET /segments/{id}/contacts | Segments. GET /segments/{id} includes contact_count. |
POST /topics, GET /topics, GET, PATCH, DELETE /topics/{id} | Topics. |
POST /contact-properties, GET /contact-properties, PATCH, DELETE /contact-properties/{id} | Properties. |
/audiences, /audiences/{id}/contacts | The older audience API, kept for existing Resend and Ha Tang clients. An audience is a segment. |
The Node.js SDK has the same calls under hatang.contacts, hatang.segments, hatang.topics, hatang.contactProperties and hatang.audiences.