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: 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/contacts

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

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

Query Parameters

tagstringoptional

Filter by tag

searchstringoptional

Search by email or name

pagenumberoptional

Page number (default: 1)

GET /v1/contacts
curl https://api.posthawk.dev/v1/contacts \
  -H "Authorization: Bearer your_api_key"
Response
{
  "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
}
GET/v1/contacts/:id

Get a single contact by ID. Requires the `reading` scope. Returns the bare contact row, or 404 if not found.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

Path Parameters

idstringrequired

Contact UUID

GET /v1/contacts/:id
curl https://api.posthawk.dev/v1/contacts/{id} \
  -H "Authorization: Bearer your_api_key"
Response
{
  "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"
}
POST/v1/contacts

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

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

Body

emailstringrequired

Contact email address

namestringoptional

Contact name

tagsstring[]optional

Array of tag strings

metadataobjectoptional

Arbitrary JSON metadata

POST /v1/contacts
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"]
  }'
PATCH/v1/contacts/:id

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

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

Body

namestringoptional

Updated name

tagsstring[]optional

Updated tags

metadataobjectoptional

Updated metadata

unsubscribedbooleanoptional

Unsubscribe status

PATCH /v1/contacts/:id
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
  }'
DELETE/v1/contacts/:id

Delete a contact by ID.

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

DELETE /v1/contacts/:id
curl -X DELETE https://api.posthawk.dev/v1/contacts/{id} \
  -H "Authorization: Bearer your_api_key"
POST/v1/contacts/import

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

Authorizations

Authorizationstring · headerrequired

Bearer authentication header of the form Bearer <token>, where <token> is your API Key.

Body

contactsarrayrequired

Array of contact objects with email, name, tags, metadata

POST /v1/contacts/import
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"
        ]
      }
    ]
  }'
Response
{
  "imported": 95,
  "skipped": 5
}