API Reference
Complete REST API reference for all public and authenticated endpoints.
Base URL
All API endpoints are relative to your deployment’s base URL, configured in _config.yml as api_base_url. It follows the format:
https://api.makeemwait.com/v1
For interactive testing, use the curl examples provided throughout this reference.
Authentication
The API supports two authentication methods:
Authentication Token
Send your ID token in the Authorization header:
Authorization: Bearer YOUR_ID_TOKEN
Tokens are obtained by logging in through the authentication SDK.
API Key
Send your API key in the x-api-key header:
x-api-key: mew_live_YOUR_API_KEY
Your API key is generated when you create your account and shown once. You can regenerate it from the Account page if needed.
Public Endpoints
These endpoints require no authentication and are used by the signup form and embed widget. All /public/* endpoints return Access-Control-Allow-Origin: * for cross-origin requests.
Create Signup
POST /public/waitlists/{waitlist_id}/signups
Add a person to a waitlist.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Email address |
first_name |
string | No | First name (required if waitlist has name_field: "required") |
last_name |
string | No | Last name (required if waitlist has name_field: "required") |
phone |
string | No | Phone number (required if waitlist has phone_field: "required") |
referred_by_token |
string | No | Referral token of the person who referred them |
consent |
boolean | No | Must be true if the waitlist has require_consent enabled |
timezone |
string | No | The signup’s timezone (e.g., America/New_York) |
utm_source |
string | No | UTM source |
utm_medium |
string | No | UTM medium |
utm_campaign |
string | No | UTM campaign |
utm_term |
string | No | UTM term |
utm_content |
string | No | UTM content |
custom_answers |
object | No | Answers to custom questions (keyed by question ID) |
website |
string | No | Honeypot field — must be left empty (bots that fill it get a fake success response) |
Response (201):
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"referral_token": "a3b2c1d4e5f67890a1b2c3d4e5f67890",
"position": 42,
"referral_count": 0,
"total_signups": 150,
"created_at": "2025-03-15T10:30:00.000Z",
"redirect_url": "https://yoursite.com/thank-you"
}
The redirect_url field is only included if the waitlist owner has configured a redirect URL (Advanced+). If the email already exists on the waitlist, the existing signup data is returned with a 200 status instead of 201.
Error Codes:
| Status | error_code |
Cause |
|---|---|---|
400 |
VALIDATION_ERROR |
Invalid email, missing required name/phone, invalid custom answer, or consent not given (consent failures return VALIDATION_ERROR with a consent field error in the errors array) |
400 |
INVALID_BODY |
Request body is not valid JSON |
403 |
WAITLIST_CLOSED |
Waitlist is not accepting signups |
400/403 |
DOMAIN_RESTRICTED |
Email domain not in allowed list (403), or personal email blocked by exclude_personal_emails (400) |
404 |
NOT_FOUND |
Waitlist not found |
Get Signup Status
GET /public/waitlists/{waitlist_id}/signups/{email}
Get a signup’s current position, referral count, and other data.
Response (200):
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"position": 42,
"referral_count": 5,
"total_signups": 150,
"created_at": "2025-03-15T10:30:00.000Z"
}
referral_token is only returned in the initial POST /signups response. It is not included in GET signup status responses.
position and total_signups fields are omitted.
Verify Email
POST /public/waitlists/{waitlist_id}/signups/{email}/verify
GET /public/waitlists/{waitlist_id}/signups/{email}/verify?token=TOKEN
Verify a signup’s email address using the verification token sent by email.
Request Body (POST):
{
"token": "a3b2c1d4e5f67890..."
}
Query Parameter (GET): token — the verification token from the email link.
Both methods accept the same token value. The GET method is used by clickable verification links in emails.
Response (200):
{
"verified": true,
"message": "Email verified successfully"
}
Returns "Email already verified" if the email was previously verified.
Error Codes:
| Status | error_code |
Cause |
|---|---|---|
400 |
VALIDATION_ERROR |
Missing token |
400 |
INVALID_TOKEN |
Token is invalid or does not match |
404 |
NOT_FOUND |
Signup or waitlist not found |
Get Waitlist Config
GET /public/waitlists/{waitlist_id}/config
Get the public configuration needed to render a signup form.
Response (200):
{
"waitlist_id": "a1b2c3d4",
"name": "Beta Access",
"headline": "Join the Beta",
"subheadline": "Be the first to try our new product",
"hero_image_url": "https://example.com/hero.png",
"name_field": "optional",
"phone_field": "hidden",
"signup_count": 150,
"custom_questions": [
{
"id": "q1",
"label": "What's your role?",
"type": "dropdown",
"required": true,
"options": ["Founder", "Developer", "Designer", "Other"]
}
],
"status": "open",
"show_social_proof": true,
"require_consent": false,
"consent_text": "I agree to the Privacy Policy and Terms of Service",
"referral_boost": 1,
"position_offset": 0,
"hide_branding": false,
"hide_position": false,
"require_email_verification": false
}
Notes:
signup_countreturnsnullif fewer than 11 signups (privacy) or if the owner hashide_positionenabledstatus—"open"or"closed". When closed, forms should show a “waitlist closed” message instead of the signup formshow_social_proof— whether to display social proof messages (default:true)require_consentandconsent_text— whether a consent checkbox is required and what text to showreferral_boost— how many positions each referral is worthposition_offset— amount added to displayed position numbers (Advanced+, 0 on lower tiers)hide_brandingandhide_positionare only returned if the owner is on Advanced+require_email_verificationis only returned if the owner is on Pro- This endpoint also increments the daily page view counter for analytics
Get Signup Count
GET /public/waitlists/{waitlist_id}/count
Response (200):
{
"waitlist_id": "a1b2c3d4",
"count": 150
}
Returns null for count if the waitlist has 10 or fewer signups or if the owner has “Hide position” enabled.
Get Leaderboard
GET /public/waitlists/{waitlist_id}/leaderboard
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 10 | Results to return (1-50) |
Response (200):
{
"waitlist_id": "a1b2c3d4",
"leaderboard": [
{
"rank": 1,
"referral_count": 12
},
{
"rank": 2,
"referral_count": 8
}
]
}
The leaderboard is fully anonymized — only rank and referral count are returned.
Unsubscribe
POST /public/waitlists/{waitlist_id}/unsubscribe
Opt a signup out of marketing emails. Requires an HMAC-verified token (included in unsubscribe links in email footers).
Request Body:
{
"email": "jane@example.com",
"token": "a3b2c1d4e5f67890..."
}
Response (200):
{
"success": true,
"message": "You have been unsubscribed from marketing emails",
"delete_token": "f8a90123b4c5d6e7..."
}
The delete_token is returned so the user can proceed to full data deletion if desired. Setting email_opt_out: true on the signup record means the user is skipped in future email blasts, referral notifications, and milestone emails. Their waitlist position and data are preserved.
Error Codes:
| Status | error_code |
Cause |
|---|---|---|
400 |
VALIDATION_ERROR |
Missing email or token |
400 |
INVALID_BODY |
Request body is not valid JSON |
403 |
INVALID_TOKEN |
Invalid unsubscribe token |
404 |
NOT_FOUND |
Waitlist or signup not found |
Delete My Data
POST /public/waitlists/{waitlist_id}/delete-my-data
Self-service data deletion. Requires an HMAC-verified delete token, which is only revealed after a successful unsubscribe.
Request Body:
{
"email": "jane@example.com",
"token": "f8a90123b4c5d6e7..."
}
Response (200):
{
"success": true,
"message": "Your data has been deleted"
}
Permanently deletes the signup record and decrements the waitlist’s signup count.
Error Codes:
| Status | error_code |
Cause |
|---|---|---|
400 |
VALIDATION_ERROR |
Missing email or token |
400 |
INVALID_BODY |
Request body is not valid JSON |
403 |
INVALID_TOKEN |
Invalid delete token |
404 |
NOT_FOUND |
Waitlist or signup not found |
Auth Endpoints
Register
POST /auth/register
Request Body:
{
"email": "jane@example.com",
"password": "securePassword123"
}
Password must be at least 8 characters.
Response (201):
{
"email": "jane@example.com",
"api_key": "mew_live_abc123...",
"subscription_tier": "pro",
"subscription_status": "trialing"
}
The api_key is shown only in this response. Save it securely — it will not be returned again. A 7-day Pro trial is started automatically.
Error Codes:
| Status | error_code |
Cause |
|---|---|---|
400 |
VALIDATION_ERROR |
Invalid email format, or password shorter than 8 characters |
400 |
INVALID_BODY |
Request body is not valid JSON |
409 |
CONFLICT |
Account with this email already exists |
Get Profile
GET /auth/me
Response (200):
{
"email": "jane@example.com",
"has_api_key": true,
"subscription_tier": "basic",
"subscription_status": "active",
"trial_ends_at": null,
"stripe_customer_id": "cus_abc123",
"has_used_trial": true,
"trial_end": null
}
Note: The raw API key is never returned from this endpoint — only has_api_key (boolean). Use POST /auth/regenerate-api-key to generate a new key.
Regenerate API Key
POST /auth/regenerate-api-key
Generate a new API key. The old key stops working immediately. Requires JWT authentication (not API key auth).
Response (200):
{
"api_key": "mew_live_new_key..."
}
Delete Account
DELETE /auth/me
Permanently deletes the authenticated user’s account and all associated data, including:
- All waitlists and their signups
- Email templates, verified domains, and team members
- Analytics and UTM data
- The Stripe customer (cancels any active subscription)
- Your user account and authentication credentials
Returns 204 No Content on success. This action cannot be undone.
Billing Endpoints
All billing endpoints require authentication.
Create Checkout Session
POST /billing/checkout
Creates a Stripe Checkout session for new subscribers, or directly upgrades/downgrades existing subscribers.
Request Body:
{
"tier": "basic",
"interval": "monthly"
}
| Field | Values |
|---|---|
tier |
basic, advanced, pro |
interval |
monthly (default), yearly |
Response for new subscribers (200):
{
"url": "https://checkout.stripe.com/c/pay/..."
}
Redirect the user to the returned URL for Stripe Checkout.
Response for existing subscribers (200):
{
"upgraded": true,
"tier": "advanced",
"status": "active"
}
If payment fails during an upgrade, returns { "upgraded": false, "payment_failed": true } and the plan is not changed.
If the user is currently trialing, the trial is canceled before creating the checkout session.
Preview Plan Change
POST /billing/preview
Preview what a plan change will cost without making any changes. Returns proration details. Requires an active paid subscription (not available during free trial).
Request Body:
{
"tier": "pro",
"interval": "monthly"
}
Response (200):
{
"current_plan": "Basic (monthly)",
"new_plan": "Pro (monthly)",
"proration_amount": 4700,
"proration_formatted": "$47.00",
"new_recurring_price": 5000,
"new_recurring_formatted": "$50.00/month",
"is_upgrade": true,
"immediate_charge": true
}
If the user is already on the requested plan, returns { "already_on_plan": true, "actual_tier": "pro" }.
Start Trial
POST /billing/start-trial
Start a 7-day Pro trial via Stripe. Each user can only use the trial once.
Response (200):
{
"status": "trialing",
"tier": "pro",
"trial_end": 1710518400
}
Cancel Subscription
POST /billing/cancel
Cancel the current subscription. Active subscriptions cancel at the end of the billing period. Trialing subscriptions cancel immediately.
Response (200) — active subscription:
{
"status": "canceling",
"canceled_immediately": false,
"cancel_at": 1713110400,
"current_period_end": 1713110400
}
Response (200) — trialing subscription:
{
"status": "inactive",
"canceled_immediately": true
}
Get Billing Info
GET /billing/info
Returns recent invoices and default payment method from Stripe.
Response (200):
{
"invoices": [
{
"id": "in_abc123",
"date": 1710518400,
"amount": 5000,
"amount_formatted": "$50.00",
"status": "paid",
"description": "Subscription",
"invoice_pdf": "https://...",
"hosted_invoice_url": "https://..."
}
],
"payment_method": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2026
}
}
Returns { "invoices": [], "payment_method": null } if the user has no Stripe customer.
Create Customer Portal Session
POST /billing/portal
Response (200):
{
"url": "https://billing.stripe.com/p/session/..."
}
Redirect to the Stripe Customer Portal for plan changes, payment method updates, and invoices.
Waitlist Endpoints
All waitlist endpoints require authentication.
Create Waitlist
POST /waitlists
Request Body:
{
"name": "Beta Access",
"settings": {
"headline": "Join the Beta",
"phone_field": "optional"
}
}
The name field is required. All settings fields are optional.
Full Settings Schema
| Field | Type | Tier | Description |
|---|---|---|---|
headline |
string (max 100) | All | Custom headline on the signup form |
subheadline |
string (max 200) | All | Text below the headline |
hero_image_url |
string | All | URL of an image displayed above the form |
name_field |
string | All | "optional" (default), "required", or "hidden" |
phone_field |
string | All | "hidden" (default), "optional", or "required" |
custom_questions |
array (max 10) | All | Custom questions (see Creating Waitlists) |
redirect_url |
string (HTTPS, max 500) | Advanced+ | Redirect URL after signup |
hide_branding |
boolean | Advanced+ | Remove “Powered by MakeEmWait” link |
hide_position |
boolean | Advanced+ | Don’t show position numbers to signups |
exclude_personal_emails |
boolean | Advanced+ | Block free email providers (Gmail, Yahoo, etc.) |
allowed_email_domains |
array of strings | Advanced+ | Only accept signups from listed domains |
webhook_url |
string (HTTPS) | Pro | Webhook URL that receives POST requests on events |
webhook_events |
array of strings | Pro | Events to fire: "signup.created", "referral.credited" |
slack_webhook_url |
string (HTTPS) | Pro | Slack incoming webhook URL |
discord_webhook_url |
string (HTTPS) | Pro | Discord webhook URL |
status |
string | All | "open" (default) or "closed" — controls whether the waitlist accepts signups |
show_social_proof |
boolean | All | Show social proof message on signup forms (default: true) |
require_consent |
boolean | All | Show a consent checkbox on signup forms |
consent_text |
string (max 500) | All | Custom consent checkbox text (supports markdown-style links) |
referral_boost |
integer (1–100) | All | Positions gained per referral (default: 1) |
position_offset |
integer (0–1,000,000) | Advanced+ | Amount added to displayed position numbers |
notify_referrer |
boolean | Pro | Email referrers when someone uses their link |
sms_notify_referrer |
boolean | Pro | SMS referrers when someone uses their link (requires phone number on file) |
require_email_verification |
boolean | Pro | Send verification token after signup |
referral_milestones |
array of integers (max 10, values 1–10,000) | Pro | Referral counts that trigger a milestone email |
email_templates |
object | Pro | Template IDs: signup_confirmation, referral_milestone, offboarding |
Using a tier-gated field on a lower plan returns 403 Forbidden with error_code: "TIER_REQUIRED".
Response (201):
{
"waitlist_id": "a1b2c3d4",
"name": "Beta Access",
"signup_count": 0,
"created_at": "2025-03-15T10:30:00.000Z"
}
List Waitlists
GET /waitlists
Response (200):
{
"waitlists": [
{
"waitlist_id": "a1b2c3d4",
"name": "Beta Access",
"signup_count": 150,
"created_at": "2025-03-15T10:30:00.000Z"
}
]
}
Get Waitlist
GET /waitlists/{waitlist_id}
Returns full waitlist details including all settings.
Response (200):
{
"waitlist_id": "a1b2c3d4",
"name": "Beta Access",
"settings": {
"headline": "Join the Beta",
"name_field": "optional",
"phone_field": "hidden",
"webhook_url": "https://example.com/webhook",
"webhook_events": ["signup.created"]
},
"signup_count": 150,
"created_at": "2025-03-15T10:30:00.000Z"
}
Update Waitlist
PATCH /waitlists/{waitlist_id}
Partial update — only provided fields are changed. Settings are merged with existing values.
Request Body:
{
"name": "Updated Name",
"settings": {
"headline": "New Headline"
}
}
Response (200):
{
"waitlist_id": "a1b2c3d4",
"name": "Updated Name",
"settings": {
"headline": "New Headline",
"name_field": "optional",
"phone_field": "hidden"
},
"signup_count": 150,
"created_at": "2025-03-15T10:30:00.000Z"
}
Delete Waitlist
DELETE /waitlists/{waitlist_id}
Returns 204 No Content. All data is permanently deleted, including signup records, analytics, and blasts.
Admin Endpoints
All admin endpoints require authentication and verify that you own the waitlist (or are a team member of the owner).
List Signups
GET /waitlists/{waitlist_id}/signups
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Results per page (1-100) |
last_key |
string | — | Base64-encoded pagination cursor from previous response |
Response (200):
{
"signups": [
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+1234567890",
"position": 1,
"referral_token": "a3b2c1d4e5f67890a1b2c3d4e5f67890",
"referral_count": 5,
"referred_by": "b4c5d6e7f8a90123b4c5d6e7f8a90123",
"utm": {
"utm_source": "twitter",
"utm_medium": "social"
},
"custom_answers": {
"q1": "Startup founder"
},
"email_verified": true,
"email_opt_out": true,
"email_opt_out_at": "2025-03-20T14:00:00.000Z",
"device_type": "desktop",
"created_at": "2025-03-15T10:30:00.000Z"
}
],
"count": 1,
"last_key": "eyJwYXJ0aXRpb..."
}
Pass the last_key value as a query parameter in your next request to paginate.
Delete Signup
DELETE /waitlists/{waitlist_id}/signups/{email}
Returns 204 No Content. Does not decrement the signup count — positions stay stable. If the waitlist has an offboarding email template assigned (Pro), the removed person receives the offboarding email automatically.
Move Position (Advanced+)
PATCH /waitlists/{waitlist_id}/signups/{email}/position
Request Body:
{
"position": 1
}
Response (200):
{
"email": "jane@example.com",
"position": 1
}
Get Statistics
GET /waitlists/{waitlist_id}/stats
Response (200):
{
"waitlist_id": "a1b2c3d4",
"total_signups": 1247,
"referral_signups": 312,
"created_at": "2025-03-01T00:00:00.000Z"
}
Export CSV (Basic+)
GET /waitlists/{waitlist_id}/export
Supports ?token=YOUR_JWT query parameter for browser download links.
Returns a CSV file with Content-Type: text/csv and Content-Disposition: attachment headers. Columns include: email, first_name, last_name, phone, position, referral_count, referral_token, referred_by, utm_source, utm_medium, utm_campaign, utm_term, utm_content, custom question labels, email_opt_out, email_opt_out_at, consent_given_at, consent_text_version, privacy_policy_version, email_verified, and created_at.
Import CSV (Basic+)
POST /waitlists/{waitlist_id}/import
Import signups from a parsed CSV file. The frontend parses the CSV client-side and sends the data as a JSON array.
Request Body:
{
"signups": [
{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+1234567890"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
signups |
array | Yes | Array of signup objects |
signups[].email |
string | Yes | Valid email address |
signups[].first_name |
string | No | First name |
signups[].last_name |
string | No | Last name |
signups[].phone |
string | No | Phone number |
Response (200):
{
"imported": 47,
"skipped": 3,
"errors": [{ "row": 12, "email": "bad@", "error": "Invalid email" }]
}
Duplicate emails are skipped. Invalid emails are reported in the errors array as objects with row, email, and error fields (capped at 50 entries). The signup counter is updated atomically after import.
Analytics Endpoints (Pro)
Traffic Analytics
GET /waitlists/{waitlist_id}/analytics
| Parameter | Type | Default | Description |
|---|---|---|---|
range |
string | 30d |
Time range: 7d, 30d, or 90d |
Response (200):
{
"waitlist_id": "a1b2c3d4",
"range": "30d",
"views_by_day": {
"2025-03-01": 150,
"2025-03-02": 220
},
"signups_by_day": {
"2025-03-01": 32,
"2025-03-02": 45
},
"referral_signups_by_day": {
"2025-03-01": 8,
"2025-03-02": 12
},
"conversion_rate_by_day": {
"2025-03-01": 21.3,
"2025-03-02": 20.5
},
"top_referrers": [
{
"email": "jane@example.com",
"first_name": "Jane",
"referral_count": 24
}
]
}
UTM Analytics
GET /waitlists/{waitlist_id}/analytics/utm
Response (200):
{
"waitlist_id": "a1b2c3d4",
"by_source": [
{ "value": "twitter", "count": 245 },
{ "value": "google", "count": 95 }
],
"by_medium": [
{ "value": "social", "count": 320 },
{ "value": "email", "count": 150 }
],
"by_campaign": [
{ "value": "launch-week", "count": 400 }
]
}
Each breakdown is sorted by count (highest first). Only utm_source, utm_medium, and utm_campaign are aggregated here. utm_term and utm_content are available per-signup via the list signups endpoint.
Email Blast Endpoints (Pro)
Send Blast
POST /waitlists/{waitlist_id}/blasts
Send a bulk email to everyone on the waitlist. You must provide at least one of html_body or text_body.
Request Body:
{
"subject": "We're launching next week!",
"html_body": "<h1>Big news!</h1><p>We're launching next Tuesday.</p>",
"text_body": "Big news! We're launching next Tuesday."
}
Response (200):
{
"blast_id": "e5f6a7b8",
"recipient_count": 1247,
"failed_count": 3,
"status": "sent"
}
Emails are sent in batches of 100 via the Resend batch API. If your account has a verified custom domain, emails are sent from that domain. Otherwise, they’re sent from noreply@makeemwait.com.
List Blasts
GET /waitlists/{waitlist_id}/blasts
Response (200):
{
"blasts": [
{
"blast_id": "e5f6a7b8",
"subject": "We're launching next week!",
"sent_at": "2025-03-15T10:30:00.000Z",
"recipient_count": 1247,
"failed_count": 3,
"status": "sent"
}
]
}
Template Endpoints (Pro)
Create Template
POST /templates
Request Body:
{
"template_type": "signup_confirmation",
"name": "Welcome Email",
"subject_template": "You're on the waitlist!",
"html_body_template": "<h1>Welcome!</h1>",
"text_body_template": "Welcome!"
}
| Field | Required | Description |
|---|---|---|
template_type |
Yes | signup_confirmation, referral_milestone, offboarding, or blast |
subject_template |
Yes | Email subject line (supports {{variable}} placeholders) |
html_body_template |
At least one | HTML version of the email body |
text_body_template |
At least one | Plain text version of the email body |
name |
No | A friendly name for the template (defaults to the type) |
Response (201):
{
"template_id": "f1a2b3c4",
"template_type": "signup_confirmation",
"name": "Welcome Email",
"subject_template": "You're on the waitlist!",
"created_at": "2025-03-15T10:30:00.000Z"
}
See Email Templates for the full list of available variables by template type.
List Templates
GET /templates
Response (200):
{
"templates": [
{
"template_id": "f1a2b3c4",
"template_type": "signup_confirmation",
"name": "Welcome Email",
"subject_template": "You're on the waitlist!",
"created_at": "2025-03-15T10:30:00.000Z"
}
]
}
Body content is not included in list responses — use the get endpoint to fetch full content.
Get Template
GET /templates/{template_id}
Response (200):
{
"template_id": "f1a2b3c4",
"template_type": "signup_confirmation",
"name": "Welcome Email",
"subject_template": "You're on the waitlist!",
"html_body_template": "<h1>Welcome!</h1>",
"text_body_template": "Welcome!",
"created_at": "2025-03-15T10:30:00.000Z"
}
Update Template
PATCH /templates/{template_id}
Only provide the fields you want to change.
Request Body:
{
"subject_template": "Updated subject line"
}
Response (200):
{
"template_id": "f1a2b3c4",
"updated": true
}
Delete Template
DELETE /templates/{template_id}
Returns 204 No Content. Waitlists that had this template assigned will stop sending that email type until a new template is assigned.
Domain Endpoints (Pro)
Start Verification
POST /domains/verify
Request Body:
{
"domain": "send.yourcompany.com"
}
Response (200):
{
"domain": "send.yourcompany.com",
"status": "pending",
"dns_records": [
{
"type": "TXT",
"name": "send._domainkey.yourcompany.com",
"value": "v=spf1 include:amazonses.com ~all",
"purpose": "SPF",
"ttl": "Auto"
},
{
"type": "CNAME",
"name": "resend._domainkey.yourcompany.com",
"value": "xxxxxxxx.dkim.resend.dev",
"purpose": "DKIM",
"ttl": "Auto"
}
]
}
Add the returned DNS records to your domain registrar. See Custom Domains for setup details.
List Domains
GET /domains
Response (200):
{
"domains": [
{
"domain": "send.yourcompany.com",
"verification_status": "verified",
"created_at": "2025-03-15T10:30:00.000Z"
}
]
}
Check Status
GET /domains/{domain}/status
Triggers a live verification check and returns the current status.
Response (200):
{
"domain": "send.yourcompany.com",
"status": "verified"
}
Delete Domain
DELETE /domains/{domain}
Returns 204 No Content. Removes the domain from both MakeEmWait and Resend.
Team Endpoints (Pro)
Invite Member
POST /team/invite
Request Body:
{
"email": "colleague@example.com",
"role": "admin"
}
| Field | Required | Description |
|---|---|---|
email |
Yes | Team member’s email address |
role |
No | "admin" or "viewer" (defaults to "viewer") |
Response (201):
{
"email": "colleague@example.com",
"role": "admin",
"invited_at": "2025-03-15T10:30:00.000Z"
}
You cannot invite yourself.
List Members
GET /team
Response (200):
{
"members": [
{
"email": "colleague@example.com",
"role": "admin",
"invited_at": "2025-03-15T10:30:00.000Z"
}
]
}
Remove Member
DELETE /team/{email}
Returns 204 No Content. The team member immediately loses access to your waitlists.
Error Responses
All errors return a JSON object with error, error_code, and request_id fields:
{
"error": "Waitlist not found",
"error_code": "NOT_FOUND",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Validation errors on endpoints like POST /auth/register and PATCH /waitlists/{id} also include an errors array with per-field details:
{
"error": "A valid email address is required. Password must be at least 8 characters",
"error_code": "VALIDATION_ERROR",
"errors": [
{ "field": "email", "message": "A valid email address is required" },
{ "field": "password", "message": "Password must be at least 8 characters" }
],
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
The request_id is a unique identifier for each request and can be used for support correlation. The error_code is a machine-readable string that clients should use for programmatic error handling instead of parsing error messages.
HTTP Status Codes
| Status | Meaning |
|---|---|
400 |
Bad request — missing or invalid parameters |
401 |
Unauthorized — missing or invalid authentication |
403 |
Forbidden — insufficient subscription tier, expired trial, or access denied |
404 |
Not found — resource does not exist |
409 |
Conflict — resource already exists |
429 |
Rate limited — too many requests. Application-level 429s include a Retry-After header. |
502 |
External service error — a third-party service (Stripe, Resend) failed |
500 |
Server error |
Error Codes
Every error response includes an error_code field. Use this value to handle errors programmatically:
| Error Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Missing or invalid request parameters. Consent failures also return VALIDATION_ERROR with a consent field error in the errors array. |
INVALID_BODY |
400 | Request body is not valid JSON |
INVALID_TOKEN |
400/403 | Verification, unsubscribe, or delete token is invalid |
(no DUPLICATE_EMAIL code) |
409 | Duplicate email during registration returns 409 CONFLICT, not a separate DUPLICATE_EMAIL error code |
AUTHENTICATION_REQUIRED |
401 | Missing or invalid authentication credentials |
FORBIDDEN |
403 | Access denied (e.g., API key auth used where JWT is required) |
TIER_REQUIRED |
403 | Feature requires a higher subscription tier |
TRIAL_EXPIRED |
403 | Free trial has ended — subscription required |
EMAIL_NOT_VERIFIED |
403 | Account email must be verified before using this endpoint |
WAITLIST_CLOSED |
403 | Waitlist is not accepting new signups |
DOMAIN_RESTRICTED |
400/403 | Email domain is not in the waitlist’s allowed list (403 for allowed_email_domains), or personal emails are blocked (400 for exclude_personal_emails) |
NOT_FOUND |
404 | Resource (waitlist, signup, template, etc.) does not exist |
CONFLICT |
409 | Resource already exists |
DUPLICATE |
409 | Duplicate resource detected |
RATE_LIMITED |
429 | Too many requests — try again later. Check Retry-After header. |
PAYMENT_REQUIRED |
400/403 | Stripe payment error (400 for card errors) or inactive subscription (403) |
EXTERNAL_SERVICE_ERROR |
502 | A third-party service (Stripe, Resend) failed |
INTERNAL_ERROR |
500 | Unexpected server error |
error field contains a human-readable message suitable for displaying to users. The error_code field is a stable machine-readable value for programmatic handling — it will not change between API versions.
Tier Gating
When a feature requires a higher tier, the error tells you which plan is needed:
{
"error": "This feature requires the Advanced plan or higher",
"error_code": "TIER_REQUIRED",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Other tier-related error codes:
TIER_REQUIRED— the feature or setting needs a higher planTRIAL_EXPIRED— free trial has ended, subscription requiredFORBIDDENwith"Please subscribe to a plan to access this feature."— subscription is inactive/canceled
Client-Side Error Handling
Use the error_code field to handle errors programmatically:
const response = await fetch("/public/waitlists/abc123/signups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "jane@example.com" }),
});
if (!response.ok) {
const { error, error_code } = await response.json();
switch (error_code) {
case "WAITLIST_CLOSED":
showMessage("This waitlist is no longer accepting signups.");
break;
case "DOMAIN_RESTRICTED":
showMessage("Please use a work email address.");
break;
case "VALIDATION_ERROR":
showMessage(error); // Display the specific validation message (includes consent errors)
break;
default:
showMessage("Something went wrong. Please try again.");
}
}
CORS
All /public/* endpoints return Access-Control-Allow-Origin: * for cross-origin requests from the embed widget. Authenticated endpoints return the configured ALLOWED_ORIGIN value in CORS headers.
Related
- Error Handling — error codes, field-level validation, and handling strategies
- Rate Limits — rate limit numbers and Retry-After header
- Security — authentication methods, CORS, and data protection