# API Reference (/api-reference/overview)



## Base URL [#base-url]

```
https://api.sendly.now
```

All API requests use this base URL.

## Authentication [#authentication]

Include your API key in the `Authorization` header:

```bash
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 [#making-requests]

### Send transactional email [#send-transactional-email]

```bash
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 [#track-event]

```bash
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 [#create-contact]

```bash
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 [#response-format]

All API responses follow a standardized format for easy parsing and error handling.

### Success response [#success-response]

Public API endpoints (`/api/emails`, `/api/track`):

```json
{
  "success": true,
  "data": {
    "contact": "cnt_abc123",
    "event": "evt_xyz789",
    "timestamp": "2025-11-30T10:30:00.000Z"
  }
}
```

Dashboard API endpoints (contacts, templates, campaigns):

```json
{
  "success": true,
  "data": {
    "id": "cnt_abc123",
    "email": "user@example.com",
    "createdAt": "2025-11-30T10:30:00.000Z"
  }
}
```

List endpoints with pagination:

```json
{
  "success": true,
  "data": {
    "items": [...],
    "nextCursor": "abc123",
    "hasMore": true,
    "total": 1000
  }
}
```

### Error response [#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`:

```json
{
  "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](/api-reference/errors) for the full code table and examples.

## Pagination [#pagination]

List endpoints support cursor-based pagination:

```bash
GET /contacts?limit=100&cursor=abc123
```

**Parameters:**

* `limit` — Number of items per page (default: 20, max: 100)
* `cursor` — Pagination cursor from previous response

**Response:**

```json
{
  "items": [...],
  "nextCursor": "def456",
  "hasMore": true,
  "total": 10000
}
```

Use `nextCursor` for the next page. When `hasMore` is false, you've reached the end.

## Rate limits [#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 [#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-reference/errors).

## API endpoints [#api-endpoints]

### Public API (transactional) [#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 [#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 [#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:

```json
{
  "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 [#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 [#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 [#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 [#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 [#events]

**GET /events** — List all events
**GET /events/names** — List unique event names

### Domains [#domains]

**GET /domains** — List verified domains
**POST /domains** — Add domain for verification
**DELETE /domains/:id** — Remove domain

## Client libraries [#client-libraries]

### Node.js [#nodejs]

```javascript
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 [#python]

```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]

```bash
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 [#whats-next]

* [Send a transactional email](/api-reference/emails/sendEmail)
* [Track an event](/api-reference/events/trackEvent)
* [Error codes reference](/api-reference/errors)
