# API Reference

Source: https://wraps.dev/docs/reference/api

OpenAPI reference for the Wraps Platform API. Browse endpoints, auth, and the interactive spec.

Interactive API Explorer

Try endpoints directly in your browser with the OpenAPI spec.

[Open Swagger UI](https://api.wraps.dev/swagger)

## Base URL

Copy for AI

All API requests are made to:

base url

```
https://api.wraps.dev
```

## Authentication

Copy for AI

Authenticate using a Bearer token in the `Authorization` header. Two methods are supported:

API Key

Prefixed with `wraps_`. Best for server-to-server and SDK usage.

Create keys in the dashboard under Settings > API Keys.

Session Token

better-auth session token. Used by the dashboard and browser-based clients.

curl examples

```
curl -X GET https://api.wraps.dev/health# Authenticated requestcurl -X GET https://api.wraps.dev/contacts \  -H "Authorization: Bearer wraps_your_api_key"
```

## Endpoints

Copy for AI

The API is organized into endpoint groups. See the interactive docs for full request/response schemas.

HealthPublic

Health check and API info

GET

Contacts

Create, update, delete, and list contacts

GETPOSTPATCHPUTDELETE

Batch

Batch email sending for broadcasts

POSTGETDELETE

Email Metrics

Aggregate email metrics (sent, delivered, opens, clicks, bounces)

GET

Email Logs

Paginated email delivery logs, with lookup by message ID

GET

Events

Custom event ingestion for triggering workflows

POST

Workflows

API-triggered workflow execution

POST

Connections

AWS account connection management

GETPOSTDELETE

Domains

Sending identity (domain) verification state, read live from SES

GET

Templates

Email/SMS template CRUD, publish to SES, and CLI sync

GETPOSTPATCH

Segments

Audience segment CRUD and condition preview (Pro plan required)

GETPOSTPATCHDELETE

Agents

Leashed agent (addressable sender) management: create, kill switch, and approval queue

GETPOST

WebhooksPublic

Receive SES delivery events

POST

UnsubscribePublic

RFC 8058 one-click unsubscribe

GETPOST

ToolsPublic

Free email deliverability tools

GETPOST

## Workflows

Copy for AI

Trigger, manage, and control workflow executions via the API. Workflows must have `triggerType: "api"` and be enabled.

### Trigger Workflow

POST`/v1/workflows/:workflowId/trigger`

Triggers a workflow for a single contact. Identify the contact by ID or email. Arbitrary data can be passed through to the workflow.

| Field | Type | Description |
| --- | --- | --- |
| `contactId` | string | Contact UUID (provide this or contactEmail) |
| `contactEmail` | string | Contact email (alternative to contactId) |
| `data` | object | Arbitrary data passed to the workflow |

trigger workflow

```
curl -X POST https://api.wraps.dev/v1/workflows/:workflowId/trigger \  -H "Authorization: Bearer wraps_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "contactEmail": "user@example.com",    "data": { "plan": "pro", "source": "api" }  }'
```

### Batch Trigger Workflow

POST`/v1/workflows/:workflowId/trigger/batch`

Trigger a workflow for multiple contacts in one request. Per-contact data is merged with common data. Duplicate contacts are automatically deduplicated.

| Field | Type | Description |
| --- | --- | --- |
| `contacts` | array | Array of `{ contactId?, contactEmail?, data? }` |
| `data` | object | Common data merged into every trigger |

batch trigger

```
curl -X POST https://api.wraps.dev/v1/workflows/:workflowId/trigger/batch \  -H "Authorization: Bearer wraps_your_api_key" \  -H "Content-Type: application/json" \  -d '{    "contacts": [      { "contactEmail": "alice@example.com", "data": { "tier": "gold" } },      { "contactEmail": "bob@example.com" }    ],    "data": { "campaign": "spring-2026" }  }'
```

### Retry Failed Execution

POST`/v1/workflows/executions/:executionId/retry`

Retries a failed workflow execution from the step where it failed. Previously completed steps are preserved. Only executions with status `failed` can be retried.

retry execution

```
curl -X POST https://api.wraps.dev/v1/workflows/executions/:executionId/retry \  -H "Authorization: Bearer wraps_your_api_key"
```

### Cancel Execution

POST`/v1/workflows/executions/:executionId/cancel`

Cancels an active workflow execution. Cleans up any pending schedulers and adjusts workflow stats.

cancel execution

```
curl -X POST https://api.wraps.dev/v1/workflows/executions/:executionId/cancel \  -H "Authorization: Bearer wraps_your_api_key"
```

## Batch Sending

Copy for AI

The `GET /v1/batch/:id` endpoint returns the full batch status including progress tracking fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Batch ID |
| `status` | string | queued, scheduled, processing, completed, failed, or cancelled |
| `channel` | string | email or sms |
| `name` | string | null | Batch name |
| `totalRecipients` | number | Total targeted recipients |
| `processedRecipients` | number | Recipients processed so far |
| `sent` | number | Successfully sent count |
| `delivered` | number | Delivered count |
| `failed` | number | Failed send count |
| `opened` | number | Opened count |
| `clicked` | number | Clicked count |
| `bounced` | number | Bounced count |
| `complained` | number | Spam complaint count |
| `suppressed` | number | Suppressed (SES suppression list) count |
| `startedAt` | string | null | ISO 8601 timestamp when processing began |
| `completedAt` | string | null | ISO 8601 timestamp when processing finished |
| `createdAt` | string | ISO 8601 creation timestamp |

batch status response

```
// GET /v1/batch/:id response{  "id": "batch_abc123",  "status": "completed",  "channel": "email",  "name": "Spring Campaign",  "totalRecipients": 5000,  "processedRecipients": 5000,  "sent": 4985,  "delivered": 4970,  "failed": 15,  "opened": 3102,  "clicked": 540,  "bounced": 12,  "complained": 1,  "suppressed": 2,  "startedAt": "2026-04-06T10:00:00.000Z",  "completedAt": "2026-04-06T10:05:32.000Z",  "createdAt": "2026-04-06T09:59:58.000Z"}
```

Counts are maintained incrementally during the send and reflect SES events received so far.

### List Batch Sends

`GET /v1/batch` lists batch sends for the organization, newest first. Query with `page`, `pageSize` (max 100), `status`, `channel`, or `search` (matches name and subject). The response envelope is `{ data, page, pageSize, total }` — `total` is the full filtered count, not the page length.

batch list response

```
// GET /v1/batch?pageSize=1 response{  "data": [    {      "id": "batch_abc123",      "name": "Spring Campaign",      "status": "completed",      "channel": "email",      "subject": "Spring is here",      "totalRecipients": 5000,      "processedRecipients": 5000,      "sent": 4985,      "delivered": 4970,      "opened": 3102,      "clicked": 540,      "bounced": 12,      "complained": 1,      "suppressed": 2,      "failed": 15,      "scheduledFor": null,      "startedAt": "2026-04-06T10:00:00.000Z",      "completedAt": "2026-04-06T10:05:32.000Z",      "createdAt": "2026-04-06T09:59:58.000Z",      "template": { "id": "tmpl_1", "name": "Spring Newsletter" },      "awsAccount": { "id": "aws_1", "name": "Production", "region": "us-east-1" }    }  ],  "page": 1,  "pageSize": 1,  "total": 42}
```

### List Recipients

`GET /v1/batch/:id/recipients` returns per-recipient outcomes. `limit` defaults to 50 and is capped at **1000** — this route returns JSON through Lambda, which has a response-size ceiling a larger page could exceed. Returns 404 (not an empty list) for a batch id that doesn't belong to your organization.

batch recipients response

```
// GET /v1/batch/:id/recipients response{  "data": [    {      "id": "msg_1",      "recipient": "user@example.com",      "status": "bounced",      "error": null,      "bounceType": "Permanent",      "bounceSubType": "General",      "sentAt": "2026-04-06T10:00:01.000Z",      "createdAt": "2026-04-06T10:00:00.000Z"    }  ],  "limit": 50,  "offset": 0,  "total": 15}
```

### Clicked Links

`GET /v1/batch/:id/clicks` returns the top clicked links for a batch send, ordered by click count and capped at **50 URLs** — `truncated` is `true` when more distinct URLs exist than were returned. Per-recipient unsubscribe and preference-centre links are aggregated into `unsubscribeCount` and excluded from `data`.

batch clicks response

```
// GET /v1/batch/:id/clicks response{  "data": [    { "url": "https://example.com/product", "count": 312 },    { "url": "https://example.com/blog", "count": 88 }  ],  "unsubscribeCount": 19,  "totalDistinctUrls": 2,  "truncated": false}
```

Use `DELETE /v1/batch/:id` to cancel a batch in `scheduled`, `queued`, or `processing` status.

## Email Metrics

Copy for AI

`GET /v1/email/metrics` returns aggregate email metrics for the organization -- sent, delivered, bounces, complaints, suppressions, opens, clicks, and rendering failures -- optionally grouped by dimension and time granularity. It shares the same per-minute API rate limit as every other endpoint; reading your own numbers is not a paid feature.

`costUsd` (on `totals` and every `data` row) is an estimate from published AWS SES rates, not a billed amount. It covers outbound sending only: non-failed sends, priced at each AWS account's current SES pricing plan and tiered by UTC calendar month. It excludes the plan base fee, data transfer, storage, Lambda, dedicated IPs, and sends not made through Wraps. Each send is charged the account's average per-email cost for that month, so rows add up to the total under any grouping. For example, an account that sends 20,000,000 in a UTC month on Essentials is charged $3,000 for the month, so a 10,000-send broadcast that month is attributed $1.50, not $1.60. `null` means the cost is unknown (no recorded plan for the account), never zero. Figures for the current month can shift as month-to-date volume grows.

| Param | Type | Default |
| --- | --- | --- |
| `start_date` | ISO 8601 date or datetime | 6 days before end\_date |
| `end_date` | ISO 8601 date or datetime | now |
| `timezone` | IANA timezone name | UTC |
| `granularity` | hourly | daily | weekly | monthly | daily |
| `dimensions` | comma-separated dimension names | none (totals only) |
| `broadcast_id, template_id, aws_account_id, domain` | comma-separated, max 100 each | \-- |

Dimensions: `period`, `domain`, `broadcast`, `template`, `source`, `account`, and `region`. There is no `tags` dimension -- SES message tags are not persisted on sends. The `template`, `source`, `account`, and `region` dimensions have no equivalent in other providers' email APIs.

terminal.sh

```
curl -X GET "https://api.wraps.dev/v1/email/metrics?start_date=2026-08-25&end_date=2026-08-31&dimensions=period&granularity=daily" \  -H "Authorization: Bearer wraps_your_api_key"
```

metrics response

```
// GET /v1/email/metrics response{  "object": "metrics",  "totals": {    "sent": 1234,    "delivered": 1200,    "bounced": 12,    "bouncedPermanent": 8,    "bouncedTransient": 4,    "bouncedUndetermined": 0,    "complained": 1,    "suppressed": 3,    "opened": 640,    "openedRaw": 705,    "clicked": 210,    "failed": 22,    "costUsd": 0.1975  },  "data": [    {      "period": "2026-08-30",      "sent": 180,      "delivered": 176,      "bounced": 1,      "bouncedPermanent": 1,      "bouncedTransient": 0,      "bouncedUndetermined": 0,      "complained": 0,      "suppressed": 0,      "opened": 92,      "openedRaw": 101,      "clicked": 30,      "failed": 2,      "costUsd": 0.0288    }  ],  "meta": {    "start_date": "2026-08-25T00:00:00.000Z",    "end_date": "2026-08-31T00:00:00.000Z",    "timezone": "UTC",    "granularity": "daily",    "dimensions": ["period"],    "cost": {      "currency": "USD",      "basis": "monthly-average",      "billingTimezone": "UTC",      "includes": ["ses-outbound-sending"],      "excludes": [        "plan-base-fee",        "data-transfer",        "storage",        "lambda",        "dedicated-ip",        "sends-not-made-through-wraps"      ],      "planSource": "current",      "plans": { "acct_abc123": "ESSENTIALS" },      "unattributed": 0    }  }}
```

`opened` excludes user agents matching a known-bot list; `openedRaw` reports the same count with no bot filter applied. `clicked` is currently unfiltered.

## Domains

Copy for AI

Sending identity (domain and email address) verification state, read live from SES on every call — there is no domain table and no caching layer, so this endpoint is slower and more rate-sensitive than the rest of the API. It is read-only: there is no create, verify, or delete route here.

### List Sending Identities

`GET /v1/domains` lists SES sending identities across the organization's connected AWS accounts. Optional `?awsAccountId=` narrows to one connected account. An account whose console-access role could not be assumed is reported with `reachable: false` in `accounts` rather than failing the whole request — check `accounts` before assuming `data` is complete.

domains list response

```
// GET /v1/domains response{  "data": [    {      "identity": "example.com",      "identityType": "DOMAIN",      "sendingEnabled": true,      "verificationStatus": "SUCCESS",      "awsAccountId": "aws_1",      "region": "us-east-1"    }  ],  "accounts": [    { "id": "aws_1", "accountId": "123456789012", "region": "us-east-1", "reachable": true }  ]}
```

### Get Sending Identity Detail

`GET /v1/domains/:identity` returns full verification and DKIM detail for one identity (a domain or an email address), searching the organization's connected AWS accounts and stopping at the first that has it. `unreachableAccountIds` lists accounts whose role could not be assumed while searching — a 200 from one account doesn't mean every connected account was consulted.

domain detail response

```
// GET /v1/domains/example.com response{  "identity": "example.com",  "identityType": "DOMAIN",  "verifiedForSending": true,  "verificationStatus": "SUCCESS",  "dkim": {    "status": "SUCCESS",    "signingAttributesOrigin": "AWS_SES",    "tokens": ["abc123", "def456", "ghi789"]  },  "mailFromDomain": { "domain": "mail.example.com", "status": "SUCCESS" },  "feedbackForwarding": true,  "configurationSet": "wraps-email-default",  "awsAccountId": "aws_1",  "region": "us-east-1",  "unreachableAccountIds": []}
```

A `503` is returned — not thrown — only when **every** connected AWS account is unreachable. Fix by running `wraps platform update-role` against the affected AWS account.

domain detail response (503)

```
// GET /v1/domains/example.com response (503){  "error": "aws_account_unreachable",  "message": "The wraps-console-access-role could not be assumed in any connected AWS account. Run `wraps platform update-role`.",  "code": "INTERNAL_ERROR"}
```

## Templates

Copy for AI

CRUD, publish, and duplicate for email/SMS templates — the same table the dashboard editor and the CLI's `wraps push`/`wraps pull` protocol read and write.

The API does not compile TSX

Supply `compiledHtml`/`compiledText` yourself (the CLI renders locally; the dashboard compiles in the editor). `POST /:id/publish` refuses a template with no `compiledHtml`. AI generation is dashboard-only — there is no generation endpoint here, and there is no `DELETE` (templates are referenced by send history, so deleting one would silently detach it).

### Get a Template

`GET /v1/templates/:id` returns full detail, org-scoped. `?source=false` omits `source`, `compiledHtml`, and `compiledText` — `sourceHash` still comes back so a caller can tell whether the source changed without fetching it. The list route (`GET /v1/templates`) never returns source, content, compiledHtml, compiledText, createdBy, or lastEditedBy — those five stay on this detail route only.

template detail response

```
// GET /v1/templates/:id response{  "id": "tmpl_abc123",  "name": "Welcome Email",  "description": null,  "subject": "Welcome to Acme!",  "previewText": "Let's get you set up",  "emailType": "transactional",  "channel": "email",  "status": "PUBLISHED",  "slug": "welcome",  "publishedAt": "2026-04-06T10:05:32.000Z",  "createdAt": "2026-04-01T09:00:00.000Z",  "updatedAt": "2026-04-06T10:05:32.000Z",  "sourceHash": "8f14e45fceea167a5a36dedd4bea2543",  "variables": [{ "name": "firstName", "fallback": "there" }],  "lastEditedFrom": "api",  "source": "export default function Welcome() { ... }",  "compiledHtml": "<html>...</html>",  "compiledText": "Welcome to Acme!..."}
```

### Update a Template — Last-Write-Wins Unless You Opt Out

`PATCH /v1/templates/:id` is a partial update. By default it's last-write-wins. Send `ifUnmodifiedSince` (an ISO 8601 timestamp) to reject the update with 409 instead, when the stored `updatedAt` is later. When `source` changes, a new template-version row is written (skipped if the source is unchanged from the latest version) — and a template last edited via this route or the dashboard cannot be silently overwritten by a CLI `push` without `force`.

PATCH conflict response

```
// PATCH /v1/templates/:id response (409){  "error": "conflict",  "message": "Template was modified after ifUnmodifiedSince",  "lastEditedFrom": "dashboard",  "updatedAt": "2026-04-06T11:00:00.000Z"}
```

### Publish to SES

`POST /v1/templates/:id/publish` creates or updates the SES email template from the row's compiled HTML/text, then marks the template PUBLISHED. Requires a subject, a connected AWS account (optionally `{ awsAccountId }` in the body to pick one), and compiledHtml already set via PATCH.

publish response

```
// POST /v1/templates/:id/publish response{  "success": true,  "sesTemplateName": "wraps-welcome-email",  "publishedAt": "2026-04-06T10:05:32.000Z"}
```

`POST /v1/templates` creates a DRAFT (always react-email format — a `sourceFormat` in the body is ignored) and 409s on a duplicate slug in the org. `POST /v1/templates/:id/duplicate` copies content, channel, and previewText into a new DRAFT named "<name> (Copy)" — slug, sesTemplateName, and publishedAt are never copied. The CLI's `GET /v1/templates/pull` is unchanged by default (omit `limit` for every template in one response); pass `limit` to opt into cursor pagination and `source=false` to omit the TSX source from every row.

## Segments

Copy for AI

Every /v1/segments route requires the Pro plan or above

This includes reads — a Free org gets 403 on every verb here, not just create. Stricter than the dashboard, which only gates creating a segment. A downgraded org's only path to its own segments is the dashboard.

A segment is a saved audience filter used for broadcast targeting and workflow entry/exit. The `condition` field is the same filter-condition tree the dashboard's segment builder produces — AND/OR groups, each holding one or more `{ field, operator, value }` filters, with optional nesting. This reference doesn't re-list every field and operator — that list lives in the dashboard's segment builder, where it can change. Use `POST /v1/segments/preview` to check whether a condition you constructed is valid before saving it.

Both `GET /v1/segments` and `GET /v1/segments/:id` compute `memberCount` live against current contacts at request time — never the segment's stored/cached count, which can drift from what a broadcast would actually send to. No response from this API ever includes `createdBy`.

Deleting a segment can return 409

`DELETE /v1/segments/:id` refuses with 409 instead of deleting when a `scheduled`, `queued`, or `processing` broadcast still targets the segment — deleting out from under a live send would silently strand its targeting. A segment referenced only by a `draft`, `completed`, `failed`, or `cancelled` broadcast can be deleted.

## Contact Topics: PATCH vs PUT

Copy for AI

Topic subscriptions support two update strategies with different semantics:

PATCH /v1/contacts/:id

**Adds** topics to existing subscriptions. No topics are removed. Safe for incremental updates from multiple sources.

PUT /v1/contacts/:id/topics

**Replaces all** subscriptions. Topics not in the new list are unsubscribed. Triggers workflow events for both additions and removals.

Both endpoints accept `topicIds` (UUIDs) and `topicSlugs` (human-readable slugs). Topics with double opt-in enabled will send a confirmation email unless the contact was previously confirmed.

PATCH vs PUT

```
# PATCH adds topics without removing existing onescurl -X PATCH https://api.wraps.dev/v1/contacts/:id \  -H "Authorization: Bearer wraps_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "topicIds": ["topic_new"] }'# PUT replaces ALL topic subscriptionscurl -X PUT https://api.wraps.dev/v1/contacts/:id/topics \  -H "Authorization: Bearer wraps_your_api_key" \  -H "Content-Type: application/json" \  -d '{ "topicIds": ["topic_a", "topic_b"] }'
```

## SDK Usage

The `@wraps.dev/client` SDK provides a typed wrapper around the API.

client.ts

```
import { createPlatformClient } from '@wraps.dev/client';const client = createPlatformClient({  apiKey: process.env.WRAPS_API_KEY,});const { data } = await client.GET('/v1/contacts/', {  params: { query: { page: '1' } },});
```

## OpenAPI Spec

Copy for AI

The full OpenAPI 3.0.3 specification is auto-generated from route definitions. Use it with any OpenAPI-compatible tool.

Interactive Docs

Browse and test endpoints in the Swagger UI.

[Open Swagger UI](https://api.wraps.dev/swagger)

JSON Spec

Import into Postman, Insomnia, Scalar, or any OpenAPI tool.

[Download OpenAPI JSON](https://api.wraps.dev/swagger/json)

## Next Steps

Rate Limits

Per-minute and daily limits by plan.

[View Rate Limits](https://wraps.dev/docs/reference/rate-limits)

Error Codes

Complete reference for CLI and SDK error codes.

[View Error Codes](https://wraps.dev/docs/reference/errors)

Platform SDK

Typed TypeScript wrapper for the API.

[View SDK Reference](https://wraps.dev/docs/client-sdk-reference)

Environment Variables

All env vars for CLI, SDKs, and CI/CD.

[View Env Vars](https://wraps.dev/docs/reference/environment-variables)
