Invites and contacts API

Add contacts to a campaign and send personal invites.

Last updated May 8, 2026

Add people to a campaign as contacts, and create personal redemption invites for those campaigns. Both resources are scoped to a single campaign — you'll always pass a campaignId.

#Endpoints

  • POST /v1/invites/create — create a campaign invite, optionally emailing it.
  • GET /v1/contacts/list — list contacts on a campaign.
  • POST /v1/contacts/create — create a contact tied to a campaign.

#Authentication

Standard Bearer auth — see Authentication and API keys.

#How invites and contacts relate

A contact is a person on a campaign — name, email, address, tags, order history. They live in the campaign's contact list and can be reused across orders.

An invite is a personal redemption token for a campaign. Each invite has a unique slug and an inviteUrl you can send to a recipient. Invites may include recipient details (name, email, company), but they aren't required to be tied to a contact record. You can create an invite for someone who isn't on the contact list, and you can have a contact who has never been invited.

If you want a person on a campaign and a redemption link to send them, you typically create the contact first, then create the invite separately — the two resources don't auto-link.

#Create an invite

POST /v1/invites/create

Body:

{
  "campaignId": "<campaign_id>",
  "sendEmailNotification": true,
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "companyName": "Analytical Engines",
  "expiresAt": "2026-12-31T23:59:59.000Z"
}

Field rules:

  • campaignId — required.
  • sendEmailNotification — required, boolean. When true, the API emails the invite to the recipient. When false, the invite is created but not delivered — you'll get an inviteUrl back to send yourself.
  • firstName, lastName — optional, max 100 chars.
  • email — optional, valid email, max 254 chars. Required when sendEmailNotification is true400 Bad Request if missing.
  • companyName — optional, max 200 chars.
  • expiresAt — optional ISO 8601 timestamp. After this, the invite cannot be redeemed.

Success returns 200:

{
  "invite": {
    "_id": "<invite_id>",
    "slug": "abc123def456",
    "inviteNumber": "000123",
    "status": "UNUSED",
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "companyName": "Analytical Engines",
    "expiresAt": "2026-12-31T23:59:59.000Z",
    "emailNotificationSent": true,
    "createdAt": "2026-05-08T16:42:00.000Z"
  },
  "inviteUrl": "https://redeem.merch.com/i/abc123def456"
}

status is one of:

| Value | Meaning | | --- | --- | | UNUSED | Invite has not yet been redeemed. | | USED | Recipient completed redemption. |

curl -X POST 'https://api.merch.com/v1/invites/create' \
  -H 'Authorization: Bearer <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "campaignId": "<campaign_id>",
    "sendEmailNotification": true,
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com"
  }'

#List contacts

GET /v1/contacts/list

Query parameters:

  • campaignId (string, required) — 400 Bad Request if missing.
  • searchText (string, optional) — match against contact names or emails.
  • tags (array of strings, optional) — filter to contacts with any of these tags.
  • inviteStatusFilter (string, optional) — invited or not_invited.
  • orderCountFilter (integer, optional) — exact match on number of orders placed by the contact.

The response sets Cache-Control: no-store — clients should not cache these results.

Returns an array of contact objects:

[
  {
    "id": "<contact_id>",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "tags": ["engineering", "vip"],
    "campaignId": "<campaign_id>",
    "street1": "1 Infinite Loop",
    "street2": "",
    "city": "Cupertino",
    "state": "CA",
    "country": "US",
    "zip": "95014",
    "dateOfBirth": "1815-12-10",
    "hireDate": "2024-01-15",
    "phone": "+1-555-0100",
    "shirtSize": "M",
    "orderCount": 2
  }
]

Optional string fields default to "" rather than null when unset.

curl -X GET 'https://api.merch.com/v1/contacts/list?campaignId=<campaign_id>&inviteStatusFilter=not_invited' \
  -H 'Authorization: Bearer <your_api_key>'

#Create a contact

POST /v1/contacts/create

Body:

{
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "campaignId": "<campaign_id>",
  "phone": "+1-555-0100",
  "companyName": "Analytical Engines",
  "street1": "1 Infinite Loop",
  "street2": "",
  "city": "Cupertino",
  "state": "CA",
  "zip": "95014",
  "country": "US",
  "dateOfBirth": "1815-12-10",
  "hireDate": "2024-01-15",
  "shirtSize": "M",
  "tags": ["engineering"],
  "profilePicture": "https://example.com/avatar.png"
}

Field rules:

  • firstName, lastName — required, 1-100 chars.
  • email — required, valid email, max 254 chars.
  • campaignId — required.
  • phone — optional, max 30 chars.
  • companyName — optional, max 200 chars.
  • street1, street2 — optional, max 200 chars.
  • city, state, country — optional, max 100 chars.
  • zip — optional, max 20 chars.
  • dateOfBirth, hireDate — optional strings.
  • shirtSize — optional, max 20 chars.
  • tags — optional, array of strings.
  • profilePicture — optional, string (URL or path).

Success returns 200:

{ "contactId": "<contact_id>" }
curl -X POST 'https://api.merch.com/v1/contacts/create' \
  -H 'Authorization: Bearer <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "campaignId": "<campaign_id>"
  }'

#Errors

  • 400 Bad Request — validation failure. Body shape is { "message": "Invalid request body", "errors": [...] } for Zod-validated routes, or { "message": "..." } for missing required parameters (e.g. campaignId is required, email is required when sendEmailNotification is enabled, Campaign ID is required).
  • 401 Unauthorized — missing, malformed, or invalid bearer token. See Authentication and API keys.
  • 500 Internal Server Error — unexpected error. Body is { "message": "..." }.

Ready to build your merch?

Custom design, production, campaigns, and global fulfillment. One partner, zero platform fees. Your custom proposal in 24 hours.

Create an account to browse, design, and request orders right away. A discovery call with your rep completes onboarding.