Skip to main content
PushMail.dev

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

StatusDescription
subscribedActive, will receive emails
unsubscribedOpted out via unsubscribe link
bouncedHard bounce detected — suppressed automatically
complainedSpam report received — suppressed automatically

Create a contact

POST /v1/contacts

ParameterTypeDescription
siteIdrequiredintegerThe site this contact belongs to
emailrequiredstringEmail address (automatically lowercased)
firstNamestringFirst name
lastNamestringLast name
customFieldsobjectKey-value pairs, e.g. {"company": "Acme"}
sourcestringHow they signed up. Defaults to "api"
tagsstring[]Tag names to apply. Tags are auto-created if new.
phonestringPhone number
companystringCompany name
jobTitlestringJob title
websitestringWebsite URL
addressLine1stringAddress line 1
addressLine2stringAddress line 2
citystringCity
statestringState or province
postalCodestringPostal or ZIP code
countrystringCountry
timezonestringTimezone (e.g. America/New_York)
birthdaystringBirthday (YYYY-MM-DD)
notesstringFree-text notes
signupUrlstringURL where the contact signed up
listsinteger[]List IDs to add the contact to
Request
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.

From a server-rendered signup form
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"
  }'
From a browser, capturing the current page
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

ParameterTypeDescription
siteIdrequiredintegerFilter by site (query param)
qstringSearch by email (partial match)
statusstringFilter by status: subscribed, unsubscribed, bounced, complained
pageintegerPage number (default: 1)
limitintegerResults per page, 1-100 (default: 50)
Request
curl "https://pushmail.dev/api/v1/contacts?siteId=1&q=jane&status=subscribed&page=1&limit=20" \
  -H "Authorization: Bearer pm_live_YOUR_KEY"
Response
{
  "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"
Response
{
  "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 Conflict if 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.

ParameterTypeDescription
siteIdrequiredintegerThe site these contacts belong to
contactsrequiredarrayArray of contact objects (1-100 items)
contacts[].emailrequiredstringEmail address
contacts[].firstNamestringFirst name
contacts[].lastNamestringLast name
contacts[].phonestringPhone number (max 50 chars)
contacts[].companystringCompany name (max 200 chars)
contacts[].customFieldsobjectKey-value pairs of custom data
contacts[].tagsstring[]Tag names to apply (auto-created if new)
contacts[].listsinteger[]List IDs to add the contact to
contacts[].signupUrlstringURL where the contact signed up (see Signup attribution)
contacts[].sourcestringHow they signed up. Defaults to "api"
Request
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" }
      }
    ]
  }'
Response
{
  "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.

Partial failure
{
  "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.

Add tags when creating a contact
{
  "siteId": 1,
  "email": "user@example.com",
  "tags": ["trial", "webinar-attendee"]
}
List tags for a site
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, or by adding the contact to a tag-triggered sequence. To manage tags on existing contacts, use the dashboard.

Lists

Lists are named groups for targeting campaigns and sequences. Unlike tags, lists must be created before you can add contacts to them.

Create a list
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"
  }'
Add contacts to a list
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.

Upload CSV for import
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"}'
Check import progress
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.

ParameterTypeDescription
siteIdrequiredintegerThe site to export from (query param)
formatstring"csv" or "json" (default: "csv")
listIdintegerFilter by list membership
tagIdintegerFilter by tag
statusstringFilter by status: subscribed, unsubscribed, bounced, complained
segmentIdintegerFilter by dynamic segment
Export as CSV
curl "https://pushmail.dev/api/v1/contacts/export?siteId=1&format=csv&status=subscribed" \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -o contacts.csv
Export as JSON
curl "https://pushmail.dev/api/v1/contacts/export?siteId=1&format=json&listId=3" \
  -H "Authorization: Bearer pm_live_YOUR_KEY"
JSON response
{
  "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

On this page