Error Handling
How the MakeEmWait API reports errors, and how to handle them in your code.
Error Response Format
Every error response from the API includes three fields:
{
"error": "This waitlist is currently closed for new signups",
"error_code": "WAITLIST_CLOSED",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
| Field | Description |
|---|---|
error |
Human-readable message suitable for displaying to end users |
error_code |
Machine-readable string for programmatic error handling |
request_id |
Unique API Gateway request ID for debugging and support |
error_code for programmatic handling — not the error message. Error codes are stable across API versions while messages may be refined.
Field-Level Validation Errors
When a request fails validation on multiple fields, the API returns a 400 response with a VALIDATION_ERROR error code and an additional errors array containing 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 errors array contains objects with:
| Field | Description |
|---|---|
field |
The name of the field that failed validation |
message |
A human-readable description of what’s wrong |
The top-level error field contains all field messages joined together with periods.
Which Endpoints Return Field Errors
| Endpoint | Fields Validated |
|---|---|
POST /auth/register |
email, password |
PATCH /waitlists/{id} |
All waitlist settings (e.g., headline, webhook_url, custom_questions) |
Handling Field Errors in JavaScript
const response = await fetch(`${API_BASE}/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const data = await response.json();
if (data.errors && data.errors.length > 0) {
// Show per-field errors next to their inputs
data.errors.forEach(({ field, message }) => {
const input = document.querySelector(`[name="${field}"]`);
if (input) {
const errorEl = input.parentElement.querySelector(".field-error");
if (errorEl) errorEl.textContent = message;
}
});
} else {
// Show the general error message
showMessage(data.error);
}
}
Error Codes Reference
Validation Errors (400)
| Error Code | When It Occurs |
|---|---|
VALIDATION_ERROR |
Missing or invalid request parameters (email, password, position, etc.). Consent failures also return VALIDATION_ERROR with a consent field error in the errors array. |
INVALID_BODY |
Request body is not valid JSON |
INVALID_TOKEN |
Verification, unsubscribe, or delete token is invalid or expired |
(no DUPLICATE_EMAIL code) |
Duplicate email during registration returns 409 CONFLICT, not 400 DUPLICATE_EMAIL |
PAYMENT_REQUIRED |
Stripe payment failed (card declined, insufficient funds, etc.). Can be returned at 400 (card errors) or 403 (inactive subscription). |
Authentication Errors (401)
| Error Code | When It Occurs |
|---|---|
AUTHENTICATION_REQUIRED |
No authentication provided, or the JWT/API key is invalid |
Authorization Errors (403)
| Error Code | When It Occurs |
|---|---|
FORBIDDEN |
Access denied — e.g., using API key auth where JWT is required |
TIER_REQUIRED |
Feature requires a higher subscription tier (Basic, Advanced, or Pro) |
TRIAL_EXPIRED |
Free trial has ended — a paid subscription is required |
EMAIL_NOT_VERIFIED |
Account email must be verified before using authenticated endpoints |
WAITLIST_CLOSED |
Waitlist is not accepting new signups |
DOMAIN_RESTRICTED |
Email domain is not in the waitlist’s allowed list (returns 403), or personal emails are blocked by exclude_personal_emails (returns 400) |
Resource Errors (404, 409)
| Error Code | When It Occurs |
|---|---|
NOT_FOUND |
Waitlist, signup, template, domain, or team member does not exist |
CONFLICT |
Resource already exists (e.g., duplicate account email) |
DUPLICATE |
Duplicate resource detected |
Rate Limiting (429)
| Error Code | When It Occurs |
|---|---|
RATE_LIMITED |
Too many requests — wait and retry. The response includes a Retry-After header with the number of seconds to wait. |
Server Errors (500, 502)
| Error Code | When It Occurs |
|---|---|
EXTERNAL_SERVICE_ERROR |
A third-party service (Stripe, Resend) failed — retry the request |
INTERNAL_ERROR |
Unexpected server error |
Handling Errors in Code
JavaScript / Fetch
async function joinWaitlist(waitlistId, email) {
const response = await fetch(
`${API_BASE}/public/waitlists/${waitlistId}/signups`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
}
);
if (!response.ok) {
const { error, error_code } = await response.json();
switch (error_code) {
case "WAITLIST_CLOSED":
return { success: false, message: "This waitlist is no longer accepting signups." };
case "DOMAIN_RESTRICTED":
return { success: false, message: "Please use a work email address." };
case "VALIDATION_ERROR":
return { success: false, message: error }; // Show the specific validation message (includes consent errors)
default:
return { success: false, message: "Something went wrong. Please try again." };
}
}
return { success: true, data: await response.json() };
}
Python / Requests
import requests
def join_waitlist(waitlist_id, email):
response = requests.post(
f"{API_BASE}/public/waitlists/{waitlist_id}/signups",
json={"email": email},
)
if not response.ok:
data = response.json()
error_code = data.get("error_code")
if error_code == "WAITLIST_CLOSED":
raise WaitlistClosedError()
elif error_code == "DOMAIN_RESTRICTED":
raise DomainRestrictedError()
elif error_code == "VALIDATION_ERROR":
raise ValidationError(data["error"])
else:
raise ApiError(data["error"])
return response.json()
cURL
curl -s -X POST "https://api.makeemwait.com/v1/public/waitlists/abc123/signups" \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com"}' | jq .
# On error, the response includes error_code:
# {
# "error": "Email is required",
# "error_code": "VALIDATION_ERROR",
# "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# }
Tier Gating
Endpoints and settings that require a specific subscription tier return TIER_REQUIRED when accessed on a lower tier:
{
"error": "This feature requires the Advanced plan or higher",
"error_code": "TIER_REQUIRED",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
The tier hierarchy is: free_trial < basic < advanced < pro
| Feature | Minimum Tier |
|---|---|
| CSV Export, CSV Import | Basic |
| Hide branding, hide position, custom redirect URL, domain restrictions, move position | Advanced |
| Analytics, email templates, email blasts, custom domains, webhooks, team management | Pro |
Related error codes for subscription issues:
| Error Code | Meaning |
|---|---|
TIER_REQUIRED |
Specific feature needs a higher plan |
TRIAL_EXPIRED |
Free trial ended — must subscribe |
FORBIDDEN |
Subscription is inactive or canceled |
External Service Errors
The API integrates with Stripe (billing) and Resend (email). When these services fail, the API returns EXTERNAL_SERVICE_ERROR with a generic message instead of leaking internal details:
{
"error": "Unable to create checkout session. Please try again.",
"error_code": "EXTERNAL_SERVICE_ERROR",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Exception: Stripe card errors (declined card, insufficient funds) return PAYMENT_REQUIRED with the Stripe-provided user-facing message, since these are designed for end users:
{
"error": "Your card was declined.",
"error_code": "PAYMENT_REQUIRED",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Request ID for Debugging
Every error includes a request_id that corresponds to the API Gateway request ID. When contacting support or investigating issues, include this ID for quick correlation with server logs.
Related
- Rate Limits — rate limit numbers and retry strategies
- API Reference — complete endpoint documentation with error codes per endpoint
- Security — authentication, bot protection, and other security measures