Contacts
Contacts are email addresses associated with a site. Each contact can have tags, lists, custom fields, and a subscription status.
The contact object
{
"id": 42,
"siteId": 1,
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"status": "subscribed",
"leadScore": 45,
"customFields": { "company": "Acme Inc" },
"source": "api",
"signupUrl": "https://example.com/pricing",
"ipAddress": null,
"subscribedAt": "2025-01-15T10:00:00.000Z",
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}Status values
| Status | Description |
|---|---|
subscribed | Active, will receive emails |
unsubscribed | Opted out via unsubscribe link |
bounced | Hard bounce detected — suppressed automatically |
complained | Spam report received — suppressed automatically |
Create a contact
POST /v1/contacts
| Parameter | Type | Description |
|---|---|---|
siteIdrequired | integer | The site this contact belongs to |
emailrequired | string | Email address (automatically lowercased) |
firstName | string | First name |
lastName | string | Last name |
customFields | object | Key-value pairs, e.g. {"company": "Acme"} |
source | string | How they signed up. Defaults to "api" |
tags | string[] | Tag names to apply. Tags are auto-created if new. |
phone | string | Phone number |
company | string | Company name |
jobTitle | string | Job title |
website | string | Website URL |
addressLine1 | string | Address line 1 |
addressLine2 | string | Address line 2 |
city | string | City |
state | string | State or province |
postalCode | string | Postal or ZIP code |
country | string | Country |
timezone | string | Timezone (e.g. America/New_York) |
birthday | string | Birthday (YYYY-MM-DD) |
notes | string | Free-text notes |
signupUrl | string | URL where the contact signed up |
lists | integer[] | List IDs to add the contact to |
curl -X POST https://pushmail.dev/api/v1/contacts \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"email": "jane@example.com",
"firstName": "Jane",
"tags": ["newsletter", "paid-user"],
"customFields": { "plan": "pro" }
}'Returns 201 on success, 409 if the email already exists for this site.
Signup attribution
Use signupUrl and source to record where a contact came from. This is essential context for drip-sequence reporting — without it, you can't tell whether a welcome series is converting subscribers from your pricing page versus your blog.
signupUrl is the specific page URL the contact used to sign up. source is a short label for the channel (e.g. "api", "form", "checkout", "webinar-2025-q2"). Both are stored on the contact record itself, so they reflect the contact's original signup, not any subsequent re-engagement.
curl -X POST https://pushmail.dev/api/v1/contacts \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"email": "jane@example.com",
"signupUrl": "https://example.com/pricing",
"source": "pricing-page-form"
}'await fetch("https://pushmail.dev/api/v1/contacts", {
method: "POST",
headers: { "Authorization": "Bearer pm_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
siteId: 1,
email: form.email.value,
signupUrl: window.location.href,
source: "web-form",
}),
});If the contact already exists, signupUrl and source are not overwritten — they always reflect the original signup. To change them later, use PUT /v1/contacts/:id.
List contacts
GET /v1/contacts
| Parameter | Type | Description |
|---|---|---|
siteIdrequired | integer | Filter by site (query param) |
q | string | Search by email (partial match) |
status | string | Filter by status: subscribed, unsubscribed, bounced, complained |
page | integer | Page number (default: 1) |
limit | integer | Results per page, 1-100 (default: 50) |
curl "https://pushmail.dev/api/v1/contacts?siteId=1&q=jane&status=subscribed&page=1&limit=20" \
-H "Authorization: Bearer pm_live_YOUR_KEY"{
"data": {
"contacts": [
{
"id": 42,
"email": "jane@example.com",
"firstName": "Jane",
"status": "subscribed",
"tags": ["signup", "trial"],
...
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}Get a contact
GET /v1/contacts/:id
Returns the contact along with their tags, lists, and recent sends.
curl https://pushmail.dev/api/v1/contacts/42 \
-H "Authorization: Bearer pm_live_YOUR_KEY"{
"data": {
"contact": {
"id": 42,
"email": "jane@example.com",
"firstName": "Jane",
"status": "subscribed",
...
},
"tags": [{ "id": 1, "name": "newsletter" }],
"lists": [{ "id": 3, "name": "Weekly Newsletter" }],
"sends": []
}
}Update a contact
PUT /v1/contacts/:id
curl -X PUT https://pushmail.dev/api/v1/contacts/42 \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Janet",
"customFields": { "plan": "enterprise" }
}'Delete a contact
DELETE /v1/contacts/:id
curl -X DELETE https://pushmail.dev/api/v1/contacts/42 \
-H "Authorization: Bearer pm_live_YOUR_KEY"Note: Returns
409 Conflictif the contact has send history. You must delete or reassign the associated sends before deleting the contact.
Bulk create or update contacts
POST /v1/contacts/bulk
Create or update up to 100 contacts in a single request. Existing contacts (matched by email within the site) are updated; new emails are created. Tags are auto-created if they don't exist, and list memberships are added automatically.
| Parameter | Type | Description |
|---|---|---|
siteIdrequired | integer | The site these contacts belong to |
contactsrequired | array | Array of contact objects (1-100 items) |
contacts[].emailrequired | string | Email address |
contacts[].firstName | string | First name |
contacts[].lastName | string | Last name |
contacts[].phone | string | Phone number (max 50 chars) |
contacts[].company | string | Company name (max 200 chars) |
contacts[].customFields | object | Key-value pairs of custom data |
contacts[].tags | string[] | Tag names to apply (auto-created if new) |
contacts[].lists | integer[] | List IDs to add the contact to |
contacts[].signupUrl | string | URL where the contact signed up (see Signup attribution) |
contacts[].source | string | How they signed up. Defaults to "api" |
curl -X POST https://pushmail.dev/api/v1/contacts/bulk \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"contacts": [
{
"email": "alice@example.com",
"firstName": "Alice",
"tags": ["newsletter"],
"lists": [3]
},
{
"email": "bob@example.com",
"firstName": "Bob",
"customFields": { "plan": "pro" }
}
]
}'{
"data": {
"created": 1,
"updated": 1,
"errors": []
}
}If some contacts fail (e.g. invalid data), they appear in the errors array with the email and error message. Successfully processed contacts are not affected by individual failures.
{
"data": {
"created": 1,
"updated": 0,
"errors": [
{ "email": "bad-data@example.com", "error": "UNIQUE constraint failed" }
]
}
}Tags
Tags are lightweight labels for grouping contacts. They're scoped per site and auto-created when you include them in a contact creation request.
{
"siteId": 1,
"email": "user@example.com",
"tags": ["trial", "webinar-attendee"]
}curl "https://pushmail.dev/api/v1/tags?siteId=1" \
-H "Authorization: Bearer pm_live_YOUR_KEY"Tags are assigned when creating a contact via POST /v1/contacts with the tags field. To manage tags on an existing contact use the per-contact endpoints below.
Add a tag to an existing contact
POST /v1/contacts/:id/tags
Find-or-creates the tag in the contact's site, applies it, and fires any tag_add-triggered sequences. Idempotent — re-applying returns 200 with alreadyTagged: true.
| Parameter | Type | Description |
|---|---|---|
tagNamerequired | string | Tag name (1–100 chars). Created if it doesn't exist for this site. |
curl -X POST https://pushmail.dev/api/v1/contacts/42/tags \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "tagName": "high-value" }'{
"data": {
"tag": { "id": 9, "name": "high-value" }
}
}{
"data": {
"alreadyTagged": true,
"tag": { "id": 9, "name": "high-value" }
}
}Remove a tag from a contact
DELETE /v1/contacts/:id/tags/:tagId
Idempotent — removing a tag that isn't applied returns { removed: false }. There is no tag_remove sequence trigger today.
curl -X DELETE https://pushmail.dev/api/v1/contacts/42/tags/9 \
-H "Authorization: Bearer pm_live_YOUR_KEY"{ "data": { "removed": true } }Lists
Lists are named groups for targeting campaigns and sequences. Unlike tags, lists must be created before you can add contacts to them.
curl -X POST https://pushmail.dev/api/v1/lists \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"name": "Weekly Newsletter",
"description": "Subscribers who opted in to weekly updates"
}'curl -X POST https://pushmail.dev/api/v1/lists/3/contacts \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "contactIds": [42] }'Bulk import
Import contacts from a CSV file. The import runs asynchronously — you get a job ID back and can poll for progress.
curl -X POST https://pushmail.dev/api/v1/imports/upload \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-F "file=@contacts.csv" \
-F "siteId=1" \
-F "listId=3" \
-F 'columnMapping={"email":"Email Address","firstName":"First Name"}'curl https://pushmail.dev/api/v1/imports/7 \
-H "Authorization: Bearer pm_live_YOUR_KEY"
# Response:
{
"data": {
"import": {
"id": 7,
"status": "processing",
"totalRows": 5000,
"processedRows": 2100,
"importedRows": 2050,
"skippedRows": 50,
"errorRows": 0
}
}
}CSV format: Include a header row. At minimum, an email column is required. Duplicates are skipped automatically.
Export contacts
GET /v1/contacts/export
Download contacts as CSV or JSON. Supports filtering by list, tag, status, or segment. Limited to 10,000 contacts per export.
| Parameter | Type | Description |
|---|---|---|
siteIdrequired | integer | The site to export from (query param) |
format | string | "csv" or "json" (default: "csv") |
listId | integer | Filter by list membership |
tagId | integer | Filter by tag |
status | string | Filter by status: subscribed, unsubscribed, bounced, complained |
segmentId | integer | Filter by dynamic segment |
curl "https://pushmail.dev/api/v1/contacts/export?siteId=1&format=csv&status=subscribed" \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-o contacts.csvcurl "https://pushmail.dev/api/v1/contacts/export?siteId=1&format=json&listId=3" \
-H "Authorization: Bearer pm_live_YOUR_KEY"{
"data": {
"contacts": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"status": "subscribed",
"phone": null,
"company": "Acme Inc",
"jobTitle": null,
"city": "San Francisco",
"state": "CA",
"country": "US",
"createdAt": "2025-01-15T10:00:00.000Z",
"customFields": { "plan": "pro" }
}
],
"total": 1
}
}CSV columns: email, firstName, lastName, status, phone, company, jobTitle, city, state, country, createdAt, customFields (as JSON string).
Next steps
- Lead Scoring -- Automatically score contacts based on engagement and custom events
- Segments -- Create dynamic audiences based on contact attributes, tags, engagement, and more
- Sequences -- Enroll contacts into automated drip campaigns
- Campaigns -- Send one-off emails to a list, tag, or segment
Authentication
PushMail uses API keys for programmatic access and session cookies for the dashboard. All API requests must be authenticated.
Sequences
Sequences are automated multi-step email flows with branching conditions. Define emails, delays, and condition branches, then enroll contacts to start the drip.