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. Whentrue, the API emails the invite to the recipient. Whenfalse, the invite is created but not delivered — you'll get aninviteUrlback to send yourself.firstName,lastName— optional, max 100 chars.email— optional, valid email, max 254 chars. Required whensendEmailNotificationistrue—400 Bad Requestif 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 Requestif 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) —invitedornot_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": "..." }.