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 Key

List contacts in the workspace. Supports tag filtering, search, and pagination.

ParameterTypeInDescription
tagstringqueryFilter by tag
searchstringquerySearch by email or name
pagenumberqueryPage 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 Key

Create or upsert a contact. If a contact with the same email exists in the workspace, it will be updated.

ParameterTypeInDescription
emailrequiredstringbodyContact email address
namestringbodyContact name
tagsstring[]bodyArray of tag strings
metadataobjectbodyArbitrary JSON metadata

Request

bash
{
  "email": "john@example.com",
  "name": "John Doe",
  "tags": ["newsletter", "beta"]
}
PATCH/v1/contacts/:idAPI Key

Update a contact by ID. Only provided fields are updated.

ParameterTypeInDescription
namestringbodyUpdated name
tagsstring[]bodyUpdated tags
metadataobjectbodyUpdated metadata
unsubscribedbooleanbodyUnsubscribe status
DELETE/v1/contacts/:idAPI Key

Delete a contact by ID.

POST/v1/contacts/importAPI Key

Bulk import up to 1000 contacts. Existing contacts (by email) are updated.

ParameterTypeInDescription
contactsrequiredarraybodyArray of contact objects with email, name, tags, metadata

Response

json
{
  "imported": 95,
  "skipped": 5
}