Errors
PostStack uses standard HTTP status codes and returns JSON error responses. All error responses follow the same format.
Error Response Format
All error responses include an error field with a human-readable message and a stable, machine-readable code you can branch on (the wording may change; the code will not):
{
"error": "Domain not found",
"code": "not_found"
}The SDK throws typed errors that you can catch and inspect:
import { PostStackError } from '@poststack.dev/sdk';
try {
await poststack.emails.send({ ... });
} catch (err) {
if (err instanceof PostStackError) {
// err.statusCode = 422
// err.code = "unprocessable_entity"
// err.requestId = "03f82111-…" (quote it to support)
// err.message = "Domain 'example.com' is not verified"
}
}HTTP Status Codes
| Status | Meaning | Description | Code |
|---|---|---|---|
200 | OK | Request succeeded. Response body contains the result. | — |
201 | Created | Resource created successfully. | — |
202 | Accepted | Request accepted and queued asynchronously. Returned by POST /emails and POST /emails/batch — the body contains the new id(s) but delivery happens out-of-band. | — |
400 | Bad Request | The request body is malformed, missing required fields, or fails validation (bad email address, too many recipients, batch over 100). | invalid_request |
401 | Unauthorized | Missing or invalid API key. | unauthorized |
403 | Forbidden | API key does not have permission for this action. | forbidden |
404 | Not Found | The requested resource does not exist. | not_found |
409 | Conflict | The resource already exists (e.g., duplicate domain or contact email). | conflict |
413 | Payload Too Large | The request body or an attachment exceeds the size limit. | payload_too_large |
422 | Unprocessable Entity | The request is well-formed but cannot be processed — e.g. the from domain is not verified, or the template is not published. | unprocessable_entity |
429 | Too Many Requests | Rate limit exceeded. Check the Retry-After header. | rate_limit_exceeded |
500 | Internal Server Error | Something went wrong on our end. Try again later. | internal_error |
503 | Service Unavailable | Temporarily unavailable (maintenance or a dependency is down). Retry with backoff. | service_unavailable |
Common Errors
Here are common error messages and how to resolve them:
| Error | Status | Resolution |
|---|---|---|
Invalid API key | 401 | Check that your API key is correct and starts with sk_live_ or sk_test_. |
API key does not have permission | 403 | Use a key with full_access permission, or upgrade the key permissions. |
Domain 'example.com' is not verified | 422 | Verify your domain DNS records before sending. Use POST /domains/:id/verify. |
Domain 'example.com' not found for this team | 422 | The from address uses a domain you have not added. Add it first with POST /domains. |
Contact already exists | 409 | A contact with this email already exists. Use PATCH to update instead. |
Rate limit exceeded | 429 | Wait and retry after the duration specified in the Retry-After header. |
emails: Too big: expected array to have <=100 items | 400 | Reduce the batch to 100 emails or fewer per request. |
Template is not published | 422 | Publish the template with POST /templates/:id/publish before using it. |
Request body is missing required fields: to | 400 | Include the "to" field as an array of email addresses. |
to.0: must be a valid email address | 400 | Check the email address format in the "from" or "to" fields. |
Rate Limits
Rate-limited endpoints (sending, and most create/update routes) carry three headers describing your current bucket so you can back off proactively instead of waiting for a 429. Limits are counted per API key and per endpoint:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window. |
X-RateLimit-Remaining | Requests still available before throttling kicks in. |
X-RateLimit-Reset | Unix epoch seconds when the current window resets. |
Retry-After | Only set on 429 responses — seconds to wait before retrying. |
When you exceed the limit the API returns a 429 with the same X-RateLimit-* headers plus Retry-After:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748430120
Retry-After: 47
Content-Type: application/json
{
"error": "Rate limit exceeded. Please try again later.",
"code": "rate_limit_exceeded"
}The SDK automatically retries 408, 429 and 5xx responses (and network errors) with exponential backoff — up to 3 retries by default, configurable with the maxRetries option.