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)
```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:
```javascript
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
```typescript
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' },
});
// Send to all contacts with a tag
await posthawk.emails.send({
from: 'hello@yourdomain.com',
tag: 'newsletter',
subject: 'Weekly Update',
html: '<h1>This week at Acme</h1>...',
});
```
### Contact Form with Subject & Message
To build a full contact form (not just email collection), store the subject and message in metadata:
```javascript
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: true` via 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.GET
/v1/contactsAPI KeyList contacts in the workspace. Supports tag filtering, search, and pagination.
| Parameter | Type | In | Description |
|---|---|---|---|
tag | string | query | Filter by tag |
search | string | query | Search by email or name |
page | number | query | Page number (default: 1) |
Response
json
{
"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
}POST
/v1/contactsAPI KeyCreate or upsert a contact. If a contact with the same email exists in the workspace, it will be updated.
| Parameter | Type | In | Description |
|---|---|---|---|
emailrequired | string | body | Contact email address |
name | string | body | Contact name |
tags | string[] | body | Array of tag strings |
metadata | object | body | Arbitrary JSON metadata |
Request
bash
{
"email": "john@example.com",
"name": "John Doe",
"tags": ["newsletter", "beta"]
}PATCH
/v1/contacts/:idAPI KeyUpdate a contact by ID. Only provided fields are updated.
| Parameter | Type | In | Description |
|---|---|---|---|
name | string | body | Updated name |
tags | string[] | body | Updated tags |
metadata | object | body | Updated metadata |
unsubscribed | boolean | body | Unsubscribe status |
DELETE
/v1/contacts/:idAPI KeyDelete a contact by ID.
POST
/v1/contacts/importAPI KeyBulk import up to 1000 contacts. Existing contacts (by email) are updated.
| Parameter | Type | In | Description |
|---|---|---|---|
contactsrequired | array | body | Array of contact objects with email, name, tags, metadata |
Response
json
{
"imported": 95,
"skipped": 5
}