Features Pricing
Start My Free Trial

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.

x
Keep Your API Key Secret Never expose your API key in client-side code. Use it only in server-to-server requests.

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"
}
i
The referral_token is only returned in the initial POST /signups response. It is not included in GET signup status responses.
i
If the waitlist owner has 'Hide position' enabled (Advanced+), the 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_count returns null if fewer than 11 signups (privacy) or if the owner has hide_position enabled
  • status"open" or "closed". When closed, forms should show a “waitlist closed” message instead of the signup form
  • show_social_proof — whether to display social proof messages (default: true)
  • require_consent and consent_text — whether a consent checkbox is required and what text to show
  • referral_boost — how many positions each referral is worth
  • position_offset — amount added to displayed position numbers (Advanced+, 0 on lower tiers)
  • hide_branding and hide_position are only returned if the owner is on Advanced+
  • require_email_verification is 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.

i
Trials are now started automatically during registration. This endpoint exists for users who registered before auto-trial was implemented.

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."
}
i
Blast emails are sent as-is — no template variable substitution is performed. The subject and body you provide are delivered to every recipient unchanged.

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
i
The 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 plan
  • TRIAL_EXPIRED — free trial has ended, subscription required
  • FORBIDDEN with "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.


  • 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