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": "[email protected]",
  "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": "[email protected]",
    "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": "[email protected]"
  }'

#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": "[email protected]",
    "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": "[email protected]",
  "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": "[email protected]",
    "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 elevate your merch?

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