SendlySendly
API Reference

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.now

All 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/track for 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 wrong
  • error.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=abc123

Parameters:

  • 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"}'

What's next

On this page