API Reference
Sendly REST API reference: base URL, API key authentication, request and response formats, pagination, rate limits, error codes and every endpoint.
Base URL
https://api.sendly.nowAll API requests use this base URL.
Authentication
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY- Secret Key (sk_*) — Required for all endpoints except
/api/track - Public Key (pk_*) — Only works with
/api/trackfor client-side event tracking
Making requests
Send transactional email
curl -X POST https://api.sendly.now/api/emails \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Hello",
"body": "<p>Your message here</p>"
}'Track event
curl -X POST https://api.sendly.now/api/track \
-H "Authorization: Bearer pk_your_public_key" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"event": "signed_up"
}'Create contact
curl -X POST https://api.sendly.now/api/contacts \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"subscribed": true,
"data": {
"firstName": "John",
"plan": "pro"
}
}'Response format
All API responses follow a standardized format for easy parsing and error handling.
Success response
Public API endpoints (/api/emails, /api/track):
{
"success": true,
"data": {
"contact": "cnt_abc123",
"event": "evt_xyz789",
"timestamp": "2025-11-30T10:30:00.000Z"
}
}Dashboard API endpoints (contacts, templates, campaigns):
{
"success": true,
"data": {
"id": "cnt_abc123",
"email": "user@example.com",
"createdAt": "2025-11-30T10:30:00.000Z"
}
}List endpoints with pagination:
{
"success": true,
"data": {
"items": [...],
"nextCursor": "abc123",
"hasMore": true,
"total": 1000
}
}Error response
Errors return a single, minimal envelope. The HTTP status code carries the
category; the body carries a human-readable message and a machine-readable
code:
{
"error": {
"message": "Project ID required",
"code": "VALIDATION_ERROR"
}
}Error fields:
error.message— Human-readable description of what went wrongerror.code— Machine-readable error code for programmatic handling
That is the whole shape. There is no success flag on errors, no
statusCode/requestId/errors[]/suggestion/timestamp field, and no
X-Request-ID header — read the HTTP status line for the category and switch on
error.code for specific handling.
On a 429 Too Many Requests, the response additionally carries rate-limit
headers: Retry-After (seconds to wait) plus X-RateLimit-Limit,
X-RateLimit-Remaining, and X-RateLimit-Reset.
See the Error Codes documentation for the full code table and examples.
Pagination
List endpoints support cursor-based pagination:
GET /contacts?limit=100&cursor=abc123Parameters:
limit— Number of items per page (default: 20, max: 100)cursor— Pagination cursor from previous response
Response:
{
"items": [...],
"nextCursor": "def456",
"hasMore": true,
"total": 10000
}Use nextCursor for the next page. When hasMore is false, you've reached the end.
Rate limits
Sendly enforces reasonable rate limits to ensure service quality:
- Email sending — 14 emails/second (AWS SES default)
- API requests — 10 requests/second per API key (sending-only keys: 5/second)
- Bulk operations — Automatically queued for processing
If you exceed limits, you'll receive a 429 Too Many Requests response with a
Retry-After header (seconds to wait) and X-RateLimit-* headers. Well-behaved
clients should honor Retry-After and back off.
Error codes
The API uses standard HTTP status codes along with a machine-readable
error.code:
400 Bad Request — Invalid request parameters, malformed body, or failed
validation (error.code is VALIDATION_ERROR for most input errors)
401 Unauthorized — Missing or invalid API key / session
403 Forbidden — Not authorized to access this resource, or the project is disabled
404 Not Found — Resource doesn't exist
429 Too Many Requests — Rate limit exceeded (see the Retry-After and X-RateLimit-* headers)
500 Internal Server Error — An unexpected error occurred
For the full list of error.code values, see the Error Codes documentation.
API endpoints
Public API (transactional)
POST /api/emails — Send transactional email(s)
- Accepts single or multiple recipients
- Template or inline content
- Variable substitution
POST /api/track — Track event for contact
- Creates/updates contact
- Tracks custom event
- Can use public key
GET /api/events — List recorded events (secret key)
Contacts
GET /contacts — List all contacts POST /contacts — Create new contact GET /contacts/:id — Get contact details PATCH /contacts/:id — Update contact DELETE /contacts/:id — Delete contact
Lists
POST /lists/:id/subscribe — Add a contact to a list POST /lists/:id/unsubscribe — Remove a contact from a list
Both accept a sending-only (pk_*) key, so they can be called from a public
subscribe or preference form the contact submits themselves.
Subscribing will not silently reverse an earlier opt-out. If the address
already holds an UNSUBSCRIBED membership on the list, the call fails:
{
"success": false,
"error": {
"code": "RESUBSCRIBE_CONFIRMATION_REQUIRED",
"message": "This contact previously unsubscribed from this list; pass allowResubscribe: true to re-subscribe them with their affirmative consent.",
"details": { "previousStatus": "UNSUBSCRIBED" }
}
}Retry with "allowResubscribe": true in the body once the contact has asked to
be re-subscribed — for example because they submitted your subscribe form
again. Do not set it for operator-initiated or bulk adds: reversing an opt-out
without the contact's consent is what the default is there to prevent.
Templates
GET /templates — List all templates POST /templates — Create new template GET /templates/:id — Get template details PATCH /templates/:id — Update template DELETE /templates/:id — Delete template
Campaigns
GET /campaigns — List all campaigns POST /campaigns — Create new campaign GET /campaigns/:id — Get campaign details PATCH /campaigns/:id — Update campaign POST /campaigns/:id/send — Send or schedule campaign POST /campaigns/:id/cancel — Cancel scheduled campaign POST /campaigns/:id/test — Send test email GET /campaigns/:id/stats — Get campaign analytics
Segments
GET /segments — List all segments POST /segments — Create new segment (Dynamic or Static) GET /segments/:id — Get segment details PATCH /segments/:id — Update segment DELETE /segments/:id — Delete segment GET /segments/:id/contacts — List segment members POST /segments/:id/members — Add contacts to a static segment (by email) DELETE /segments/:id/members — Remove contacts from a static segment (by email)
Workflows
GET /workflows — List all workflows POST /workflows — Create new workflow GET /workflows/:id — Get workflow details PATCH /workflows/:id — Update workflow DELETE /workflows/:id — Delete workflow GET /workflows/:id/executions — List workflow executions
Events
GET /events — List all events GET /events/names — List unique event names
Domains
GET /domains — List verified domains POST /domains — Add domain for verification DELETE /domains/:id — Remove domain
Client libraries
Node.js
const SENDLY_SECRET_KEY = process.env.SENDLY_SECRET_KEY;
async function sendEmail(to, subject, body) {
const response = await fetch('https://api.sendly.now/api/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SENDLY_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ to, subject, body })
});
const data = await response.json();
if (!response.ok) {
throw new Error(`[${data.error.code}] ${data.error.message}`);
}
return data.data;
}Python
import os
import requests
SENDLY_SECRET_KEY = os.environ['SENDLY_SECRET_KEY']
def send_email(to, subject, body):
response = requests.post(
'https://api.sendly.now/api/emails',
headers={
'Authorization': f'Bearer {SENDLY_SECRET_KEY}',
'Content-Type': 'application/json'
},
json={'to': to, 'subject': subject, 'body': body}
)
data = response.json()
if not response.ok:
error = data['error']
raise Exception(f"[{error['code']}] {error['message']}")
return data['data']cURL
curl -X POST https://api.sendly.now/api/emails \
-H "Authorization: Bearer $SENDLY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "subject": "Hello", "body": "Message"}'