Contacts
Manage workspace-scoped contacts with tags, metadata, and unsubscribe status. Send emails to groups of contacts by tag.
Contacts let you maintain an audience within Posthawk. Each contact has an email, optional name, tags (string array), and arbitrary metadata (JSON).
Key features:
- Workspace-scoped — contacts are isolated per workspace
- Tags — organize contacts into groups (e.g. "newsletter", "beta-users")
- Send by tag — use the "tag" field in POST /v1/send to send to all contacts with that tag
- Unsubscribe — contacts with unsubscribed: true are automatically excluded from tag-based sends
- Upsert — creating a contact with an existing email updates the record
- Bulk import — import up to 1000 contacts at once
---
Adding Contacts from Your Own Forms
You can collect contacts (newsletter signups, contact forms, waitlists, etc.) from any frontend by calling the Posthawk API with your API key.
Step 1 — Create an API Key
Go to Settings → API Keys in the Posthawk dashboard and create a new key with the "sending" scope. Copy the key (it starts with ck_live_ or ck_test_).
Step 2 — Build Your Form (HTML)
<form id="signup-form">
<input type="email" name="email" placeholder="you@example.com" required />
<input type="text" name="name" placeholder="Your name" />
<button type="submit">Subscribe</button>
</form>
<script>
document.getElementById('signup-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const res = await fetch('https://api.posthawk.dev/v1/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
email: form.email.value,
name: form.name.value,
tags: ['newsletter'],
}),
});
if (res.ok) {
form.reset();
alert('Subscribed!');
}
});
</script>Step 3 — Server-Side (Node.js / Express)
For production use, call the API from your backend to keep your API key secret:
app.post('/subscribe', async (req, res) => {
const { email, name } = req.body;
const response = await fetch('https://api.posthawk.dev/v1/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + process.env.POSTHAWK_API_KEY,
},
body: JSON.stringify({
email,
name,
tags: ['newsletter'],
metadata: { source: 'website-footer' },
}),
});
if (response.ok) {
res.json({ success: true });
} else {
const data = await response.json();
res.status(400).json({ error: data.message });
}
});Step 4 — Using the TypeScript SDK
import { Posthawk } from 'posthawk';
const posthawk = new Posthawk('ck_live_...');
// Add a contact
await posthawk.contacts.create({
email: 'user@example.com',
name: 'Jane Doe',
tags: ['newsletter', 'early-access'],
metadata: { source: 'signup-page' },
});
// List contacts filtered by tag
const { data } = await posthawk.contacts.list({ tag: 'newsletter' });To send to everyone carrying a tag, use the singular tag selector on the REST POST /v1/send endpoint (recipients are resolved from your contacts). Note: the SDK's emails.send() takes explicit to recipients — tag-based recipient resolution is a REST-API feature.
Contact Form with Subject & Message
To build a full contact form (not just email collection), store the subject and message in metadata:
await fetch('https://api.posthawk.dev/v1/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
body: JSON.stringify({
email: 'visitor@example.com',
name: 'Jane',
tags: ['contact-form'],
metadata: {
subject: 'Partnership inquiry',
message: 'Hi, I would like to discuss...',
source: 'contact-page',
},
}),
});You can then view all contact form submissions in the Posthawk dashboard under Contacts, filtered by the "contact-form" tag.
Tips
- Upsert behavior: If the email already exists in your workspace, the contact is updated (tags merged, metadata overwritten). No duplicates.
- Rate limiting: The API is rate-limited per API key. For high-traffic forms, add a CAPTCHA (like Cloudflare Turnstile) on your frontend.
- Unsubscribe: Set
unsubscribed: truevia PATCH /v1/contacts/:id to exclude a contact from tag-based sends. - Bulk import: Use POST /v1/contacts/import to upload up to 1000 contacts at once from a CSV or database export.
/v1/contactsList contacts in the workspace. Supports tag filtering, search, and pagination.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Query Parameters
tagstringoptionalFilter by tag
searchstringoptionalSearch by email or name
pagenumberoptionalPage number (default: 1)
curl https://api.posthawk.dev/v1/contacts \
-H "Authorization: Bearer your_api_key"{
"contacts": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Doe",
"tags": ["newsletter", "beta"],
"metadata": {},
"unsubscribed": false,
"created_at": "2025-06-15T10:30:00.000Z"
}
],
"total": 1
}/v1/contacts/:idGet a single contact by ID. Requires the `reading` scope. Returns the bare contact row, or 404 if not found.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Path Parameters
idstringrequiredContact UUID
curl https://api.posthawk.dev/v1/contacts/{id} \
-H "Authorization: Bearer your_api_key"{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"name": "John Doe",
"tags": ["newsletter", "beta"],
"metadata": {},
"unsubscribed": false,
"created_at": "2026-06-15T10:30:00.000Z"
}/v1/contactsCreate or upsert a contact. If a contact with the same email exists in the workspace, it will be updated.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Body
emailstringrequiredContact email address
namestringoptionalContact name
tagsstring[]optionalArray of tag strings
metadataobjectoptionalArbitrary JSON metadata
curl -X POST https://api.posthawk.dev/v1/contacts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"email": "john@example.com",
"name": "John Doe",
"tags": ["newsletter", "beta"]
}'/v1/contacts/:idUpdate a contact by ID. Only provided fields are updated.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Body
namestringoptionalUpdated name
tagsstring[]optionalUpdated tags
metadataobjectoptionalUpdated metadata
unsubscribedbooleanoptionalUnsubscribe status
curl -X PATCH https://api.posthawk.dev/v1/contacts/{id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"name": "Jane Doe",
"tags": [
"newsletter",
"beta"
],
"metadata": {
"source": "website"
},
"unsubscribed": false
}'/v1/contacts/:idDelete a contact by ID.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
curl -X DELETE https://api.posthawk.dev/v1/contacts/{id} \
-H "Authorization: Bearer your_api_key"/v1/contacts/importBulk import up to 1000 contacts. Existing contacts (by email) are updated.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your API Key.
Body
contactsarrayrequiredArray of contact objects with email, name, tags, metadata
curl -X POST https://api.posthawk.dev/v1/contacts/import \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"contacts": [
{
"email": "user@example.com",
"name": "Jane Doe",
"tags": [
"newsletter"
]
}
]
}'{
"imported": 95,
"skipped": 5
}