diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b2a6b9..fdab795 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,325 @@
All notable changes to `sendly-sdk` are documented here. This project follows
[Semantic Versioning](https://semver.org/).
+## 1.1.0 — 2026-09-05
+
+Four new resources, the `/api/v1` half of six that only had a legacy one, and a
+set of renames the platform made on the wire. Most of this release is additive,
+but the renames are breaking, so it is a major-in-spirit minor: 1.1 talks to an
+API that 1.0 did not.
+
+> **The platform deploy this release waited on has shipped** — monorepo commit
+> `57826bad`, deployed 2026-09-06 — so 1.1 is releasable. The wire now speaks
+> `emailCategory`, `payload` on the v1 event write, and `mailFromDomainStatus`,
+> which is what these types send. Anyone still running the pre-`57826bad`
+> platform should stay on 1.0: an SDK sending `emailCategory` at an API that
+> still expects `type` is answered `422 validation_error` on every template and
+> campaign write.
+>
+> The vendored `openapi.json` is that released contract byte for byte, taken
+> from the monorepo at `57826bad` rather than synced from the deployed API —
+> see `scripts/sync-spec.mjs` for why production is never the source.
+
+### Breaking
+
+- **`Template.type` and `Campaign.type` are now `emailCategory`** on the legacy
+ dialect and `email_category` on v1. It affects `templates.create`,
+ `templates.update`, the `emailCategory` filter on `templates.list`, and
+ `campaigns.create`. Rename the key in the body; the values are unchanged
+ except that the enum member **`HEADLESS` is now `SELF_MANAGED_UNSUBSCRIBE`** —
+ the old name said how the mail was built, the new one says what the recipient
+ gets, which is the fact a caller is choosing between.
+
+ Nothing is accepted under both names, deliberately: an alias would let a
+ half-migrated codebase keep compiling while the two spellings drifted apart.
+
+- **`events.record`'s payload field is now `payload`, not `data`.** Only the v1
+ write moved. **`events.track` is unaffected** and still takes `data`, because
+ it is the legacy `POST /api/track` and its body is a different schema that was
+ not part of this rename. The SDK reports what each endpoint actually accepts
+ rather than smoothing the two together — a shared name here would be a lie
+ about one of them.
+
+- **`Domain.mailFromStatus` is now `mailFromDomainStatus`** (and
+ `mail_from_domain_status` on the v1 document). It sits beside `mailFromDomain`
+ and is the status _of that domain_, which the old name did not say.
+
+- **`emails.get` returns a different body — read this one.** It used to hand back
+ the whole database row plus an `events` array that was the **wrong relation**:
+ the custom analytics events a caller records with `events.record`, not the
+ delivery history the operation has always promised. A caller polling it for
+ delivery state was reading somebody else's data and, if their project recorded
+ no custom events, an empty array that looked like "nothing has happened yet".
+
+ It now returns an explicit field list, `events` as the delivery timeline
+ (`EmailEvent[]`, oldest first), and `to` filled from the joined contact — a
+ field the spec had always declared and the response had never carried.
+
+ Fields that used to leak out of it and no longer do: `bodyHash`, `dedupKey`,
+ `idempotencyKey`, `linkMap`, `sesMessageId`, `sesInboundMessageId`, `body` and
+ `headers`. Four of those are ledger keys for deduplication and idempotency and
+ the rest are internal routing state or the rendered message; none was ever
+ documented. What to change: read `events.list` if you wanted custom events, and
+ keep your own copy of the body if you were reading it back out of here.
+
+- **`emails.list` and `emails.cancelSchedule` narrowed the same way.** All three
+ handlers on that surface were returning the whole database row and each had got
+ there separately; they share one field list now. The list was the widest of
+ them, since it leaked a page of rows at a time, and `cancelSchedule` returned
+ the `dedupKey` in the same response that released it. The same eight fields
+ named above are gone from both, and both now carry `to`.
+
+ `emails.cancelSchedule` resolves `EmailResponse`, which is what the contract has
+ always published for it — the SDK had typed it as an empty envelope since 1.0,
+ so this is the type catching up to the document AND the route catching up to the
+ type.
+
+- **`sentAt`, `deliveredAt` and `bouncedAt` are now declared on `Email`.** They
+ were reaching callers only because of the whole-row leak above and were in no
+ published schema, so the honest options were to declare them or drop them.
+ Declared: they are ordinary delivery facts and callers read them. They are
+ nullable, and null means the transition has not happened.
+
+- **`EmailGetResponse` is gone, split in two.** It named the operation rather
+ than the shape, and was then reused by an operation that is not a GET. There
+ are now `EmailResponse` (a single email) and `EmailDetailResponse` (an email
+ plus its delivery events), and **`emails.get` resolves `EmailDetailResponse`**.
+ A caller who imported the old alias picks the one that matches what they read.
+
+- **Engagement left the delivery status enum.** `OPENED`, `CLICKED` and
+ `COMPLAINED` are no longer delivery states on the platform, so they are no
+ longer members of the status type behind `email.status` or the `status` filter
+ on `emails.list`. The remaining members are `PENDING`, `SENDING`, `SENT`,
+ `DELIVERED`, `RECEIVED`, `BOUNCED`, `FAILED`, `REJECTED`, `RENDERING_FAILURE`,
+ `DELIVERY_DELAY` and `CANCELLED`.
+
+ Read engagement from `openedAt` / `clickedAt` / `complainedAt` and the `opens`
+ / `clicks` counters instead. The two were one enum, which meant a message that
+ had been opened stopped reporting that it had been delivered — a status can
+ only hold one value, and delivery and engagement are not alternatives.
+
+- **The double-opt-in confirmation route moved** from `/api/lists/confirm` to
+ `/api/lists/confirm-subscription`. `lists.subscribe` documents that URL because
+ Sendly does not send the confirmation email — your application does — so a
+ caller who builds it by hand must change the path. The `confirmToken` in the
+ response is unchanged.
+
+- **`Domain.name` is now `Domain.domain`**, and the record no longer carries a
+ ready-made `dkim` array of `{type, name, value}` records. What SES actually
+ hands back is a list of tokens, so that is what is published:
+ **`dkimTokens`**, the strings to publish as CNAME records. The old shape
+ implied Sendly knew the full record set; it knew the tokens and was
+ assembling the rest.
+
+- **`DomainVerificationStatus` reports one status per DNS record type.** `dkim`
+ and `mxRecords` are gone; `dkimStatus`, `spfStatus` and `dmarcStatus` take
+ their place, and `domain`, `status` and `mailFromDomain` are now required.
+ `status` is SES's own raw DKIM state (`Success`, `Pending`) and the three
+ `*Status` fields are this platform's DNS check — both are published because
+ they can disagree, and a single collapsed verdict hid which record was
+ actually failing.
+
+- **The legacy suppression list answers a bare body.** `GET /api/suppression`
+ returns `{ items, nextCursor }` with no `{ success, data }` envelope, where it
+ previously published `{ success, data, hasMore, cursor }`. `suppression.list`
+ hands the body back untouched, so read `page.items` and `page.nextCursor`.
+ `nextCursor` is `null` on the last page and is never omitted.
+
+- **`webhooks.create` nests the endpoint beside the secret.** `data` is now
+ `{ webhook, secret }` rather than the webhook's fields spread alongside
+ `secret`. `created.data.secret` is unchanged; the endpoint's id moved to
+ `created.data.webhook.id`. Spreading a resource and a one-time credential into
+ one object made it impossible to hand the record onward without carrying the
+ secret with it.
+
+- **`Webhook.lastFour` is gone.** A webhook record now states that it never
+ carries a secret, and a four-character fragment of one is still a fragment of
+ one. Nothing identified an endpoint by it — `id` and `url` do that.
+
+### Added
+
+- **The `/api/v1` half of six resources that had only a legacy one.** Both
+ dialects stay reachable, so the versioned methods carry a `V1` suffix:
+ - `contacts` — `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`,
+ `deleteV1`, and `topicPreferences`.
+ - `lists` — `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`,
+ `deleteV1`, and `startValidationRun`.
+ - `templates` — `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`,
+ `deleteV1`.
+ - `domains` — `listV1`, `listAllV1`, `createV1`, `getV1`, `verifyV1`,
+ `deleteV1`, plus the legacy `assignStream`.
+ - `webhooks` — `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`,
+ `deleteV1`, `rotateSecretV1`.
+ - `suppression` — `listV1`, `listAllV1`, `createV1`, `getV1`, `deleteV1`.
+
+ The suffix is not decoration. The two halves answer the same question with
+ different envelopes (`{ success, data }` versus the bare body), different field
+ cases (camelCase versus snake_case) and different error bodies (the legacy
+ envelope versus RFC 9457), so a call site that mixes them up reads a `data`
+ that is not there and fails at runtime rather than at the type check. Naming
+ them apart is what makes that impossible.
+
+ One difference inside suppression is worth knowing before you swap: the v1 path
+ parameter is an **address**, and v1 answers `404 resource_not_found` for an
+ address that is not suppressed, where the legacy `suppression.get` answers
+ `200 { suppressed: false }`. Both are definite; only one of them throws.
+
+- **`sendly.topics`** — `list`, `listAll`, `create`, `get`, `update`,
+ `setSubscription`. The consent vocabulary a project mails against: a contact
+ subscribes to a topic rather than to a campaign, so switching one off silences
+ a whole audience. Two things a caller needs:
+ - **Subscribing somebody through the API does not bypass confirmation.**
+ `setSubscription({ subscribed: true })` parks the contact at `pending` and
+ answers a `confirmation_url` that **your** application delivers, from your
+ own verified domain; nothing is mailed on the topic until someone opens it.
+ There is no parameter to skip that, because a subscription an API caller
+ asserts is not evidence the mailbox holder agreed.
+ - **A topic is archived, never deleted.** There is no `delete` method because
+ there is no delete route: a topic is where people's answers are recorded, so
+ deleting it would delete the choices they made against it.
+ `update(id, { archived: true })` retires it and keeps them.
+
+- **`sendly.snippets`** — `create`, `list`, `get`, `update`, `delete`. Reusable
+ body fragments a template includes with `{{> name}}`, on the legacy dialect,
+ gated by the same `templates:*` scopes as the templates that include them —
+ a snippet is part of a template body rather than a resource with an audience of
+ its own. Deleting one does not break its templates: an absent snippet renders
+ as an empty string, like an absent variable.
+
+- **`sendly.validation`** — `validateEmails`, `getRun`, `listResults`,
+ `listResultsAll`. **Billed per address checked**: every entry in
+ `validateEmails({ emails })` costs money, so looping it over a contact list is
+ looping over your invoice. Validate a whole list with
+ `lists.startValidationRun`, a background job, and poll it with `getRun`.
+
+ A verdict of `unknown` is deliberately a distinct value from `undeliverable`:
+ it means DNS did not answer in time, so the address was **not checked**. That
+ separation exists so a DNS timeout is never grounds for deleting a contact.
+
+- **`sendly.deliverability`** — `diagnose`, `listDomainStats`,
+ `listDomainStatsAll`, `listDmarcReports`, `listDmarcReportsAll`.
+ `listDomainStats` is per **recipient** domain (`gmail.com`, `outlook.com`) —
+ the domains you send **to** — which is the axis `diagnose` cannot report: its
+ project-wide rates hide one provider refusing nearly everything while the rest
+ of your mail is healthy. DMARC reports arrive only for a policy domain the
+ project has registered, and receivers send them on their own schedule, so **an
+ empty list is correct rather than broken**.
+
+- **`campaigns.listFailures`, `campaigns.listFailuresAll` and
+ `campaigns.retryFailed`.** `stats` says how many sends failed; only these say
+ who, and `reason` comes from a fixed vocabulary rather than the underlying
+ error text so it is stable enough to branch on. `retryFailed` re-drives **only**
+ the recipients whose send failed — nobody who already received the campaign is
+ mailed again, because each ledger row is claimed before it is touched and a row
+ whose email exists already is re-queued rather than re-sent. Uniquely among v1
+ lists, `listFailures` also carries `total`: `retryFailed` acts on that number,
+ and `has_more` alone cannot tell you whether 3 or 30,000 sends failed.
+
+- **`workflows.getGraph`, `workflows.replaceGraph`, `workflows.clone`,
+ `workflows.pause` and `workflows.resume`.**
+ - `replaceGraph` is a `PUT` because a graph is replaced whole: nodes _plus_ the
+ edges between them, so a partial edit to a step list has no meaning without
+ the transitions that reference it. An id you omit deletes that step and its
+ run history; it is refused with `409 conflict` while executions are running.
+ - `clone` always creates the copy **disabled**, whatever the original was — a
+ clone exists to be reviewed, and one that started live would match the same
+ trigger events as its original from the moment it appeared.
+ - `pause` cancels every `RUNNING`/`WAITING` execution and reports how many in
+ `cancelled_executions`. `resume` re-opens the workflow to new runs and does
+ **not** restore the cancelled ones (`cancelled_executions` is always 0
+ there). That asymmetry is the point of having both: `update({ enabled:
+false })` stops new runs and leaves every in-flight contact walking the
+ graph, `pause` stops the sends already in flight, and nothing puts them back.
+
+- **`mailboxes.sendMessage` and `mailboxes.draftMessage`.** The mailbox resource
+ is no longer read-only.
+ - `sendMessage` **really sends**, as that mailbox's own address, over its own
+ domain, and the recipient can reply. There is no `from` field on purpose: a
+ route that sends under a customer's identity must not take that identity as
+ an argument. `body` is plain text and HTML is refused, so text becomes markup
+ in exactly one place. Needs `mailboxes:send`.
+ - `draftMessage` asks Sendly's assistant to **write** text and hands it back.
+ It stores nothing and sends nothing — the response reports `sent: false`, and
+ no argument changes that — so it needs only `mailboxes:read`. A client that
+ may draft is not thereby a client that may mail your customers.
+
+- **Auto-pagination for every new cursor list.** The `*All` companions now number
+ seventeen: the six from 0.3.0 plus `campaigns.listFailuresAll`,
+ `contacts.listAllV1`, `deliverability.listDmarcReportsAll`,
+ `deliverability.listDomainStatsAll`, `domains.listAllV1`, `lists.listAllV1`,
+ `suppression.listAllV1`, `templates.listAllV1`, `topics.listAll`,
+ `validation.listResultsAll` and `webhooks.listAllV1`.
+
+- **`intake_configured` on the DMARC report list**, and it is the field that
+ makes an empty page readable. `deliverability.listDmarcReports` answering
+ `data: []` used to mean either "no receiver has reported a failure" or "this
+ deployment has no report intake mailbox, so nothing can ever arrive", and the
+ two were indistinguishable. `intake_configured: false` is the second one.
+ Read it before telling anyone the domains are clean.
+
+- **`Suppression.scope`** — `PROJECT` or `GLOBAL`. Every record this API creates
+ or returns today is `PROJECT`; `GLOBAL` is a platform-wide block recorded
+ outside your project, which is why `suppression.getV1` can answer `200` for an
+ address you never suppressed yourself.
+
+- **`Template.currentVersion`** — a counter incremented by an update that changes
+ the rendered content, and left alone by one that only renames. A campaign
+ records the version it sent, so this is how a caller tells "the template
+ changed since" from "the template was retitled".
+
+- **`Webhook.domains`** — the sending domains an endpoint is scoped to, empty
+ meaning every domain on the project. It was already enforced; it is now
+ readable, so a caller can see why an endpoint is quiet.
+
+- **`Webhook.previousSecretExpiresAt`** on the record itself, not only on the
+ rotation response. While a rotation is in flight it says when the OLD secret
+ stops being accepted, and it is `null` outside one — so a verifier can tell
+ from a plain read whether it is inside a dual-signature window.
+
+- **Every `{id}` path parameter declares `format: uuid`,** and the seven
+ operations that had no `404` published now publish one. `GET
+/api/v1/contacts/{id}/topics`, `POST /api/v1/lists/{id}/validation-runs`,
+ `GET` and `PATCH /api/v1/topics/{id}`, `POST /api/v1/topics/{id}/subscriptions`,
+ `GET /api/v1/validation-runs/{id}` and its `/results` all answered
+ `404 resource_not_found` already; the contract now says so, which is what the
+ generated types and the error-handling examples are read from.
+
+### Fixed
+
+- **README: `emails.list` was destructured wrongly.** The example read
+ `page.data.items` and `page.data.cursor`; the response is
+ `{ success, data: Email[], nextCursor }`, so it is `page.data` and
+ `page.nextCursor`. Copying the old example did not compile.
+- **README: `webhooks.create` was destructured wrongly.** The legacy create
+ resolves the envelope, so the secret is at `created.data.secret`, not
+ `const { webhook, secret } = ...`. That destructuring is correct for
+ `webhooks.createV1`, which is where the example now lives.
+- **README: the mailbox resource was described as read-only** in three places.
+ It is not, since `sendMessage` and `draftMessage`; what stays out of reach is
+ the mailbox _lifecycle_, which is a different claim.
+
+### Notes
+
+- **Pagination is uniform again.** Every v1 list takes `after` and answers
+ `next_cursor`. `topics.list` and `validation.listResults` were the two
+ exceptions through 1.0 — they took `cursor` and answered `cursor` — and the
+ platform collapsed that to one dialect for this release, so both now route
+ through `paginateCursor` like every other collection. **This is breaking for a
+ caller driving those two by hand**: pass `after` instead of `cursor`, and read
+ `next_cursor` instead of `cursor`. Anyone using `topics.listAll` or
+ `validation.listResultsAll` is unaffected.
+- **`NOT_SDK_CALLABLE` is unchanged.** Creating and deleting a mailbox, creating
+ and revoking an app password, the four API-key operations, and creating a
+ project still resolve the acting user from a session and answer `401` to any
+ API key. The two new mailbox methods are the opposite case — they publish
+ `ApiKeyAuth` outright.
+- **Nothing added here takes an `idempotencyKey`.** The set of writes that accept
+ one is the same as in 1.0: `emails.send`, `emails.sendLegacy`, `emails.batch`,
+ `contacts.create`, `contacts.upsert`, `contacts.bulkCreate`,
+ `campaigns.create` and `campaigns.send`. `campaigns.retryFailed` is guarded
+ instead by a `409 conflict` on a retry already running, which is a better fit:
+ the thing to prevent is two concurrent walks, not a replayed request.
+
## 1.0.0 — 2026-09-02
The default send moves to the versioned API. Everything else in this release is
diff --git a/README.md b/README.md
index e949d45..4c27932 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,12 @@
Official TypeScript SDK for the [Sendly](https://sendly.now) REST API.
-Type-safe email, contact, domain, template, webhook, and suppression
-operations, plus mailbox and project reads and the versioned `/api/v1`
-surface — campaigns, segments, workflows, analytics, and usage. Generated from
-the public OpenAPI spec, so every endpoint and schema stays in sync.
+Type-safe email, contact, list, topic, domain, template, snippet, webhook and
+suppression operations; mailbox reads plus the two composition calls a key may
+drive; address validation and deliverability reporting; and the versioned
+`/api/v1` surface — campaigns, segments, workflows, analytics, and usage.
+Generated from the public OpenAPI spec, so every endpoint and schema stays in
+sync.
> This repository is the official standalone home and source of truth for the
> Sendly TypeScript SDK — issues and PRs are welcome here. Its surface is
@@ -69,6 +71,77 @@ const receipt = await sendly.emails.send({
console.log(receipt.id, receipt.status); // status is a real delivery state
```
+## Upgrading from 1.0
+
+1.1 is mostly additive — four new resources and the `/api/v1` half of six more —
+but it also tracks a set of **wire-visible renames** that landed in the platform,
+so it is a breaking release. Do not deploy 1.1 against an API that has not taken
+the renamed wire yet: the old field names are gone from these types, and sending
+`type` where the API now expects `emailCategory` is a `422`, not a shrug.
+
+What to change, in the order a codebase usually hits it:
+
+- **`type` → `emailCategory` (legacy) / `email_category` (v1)** on templates and
+ campaigns. Affects `templates.create`, `templates.update`, the `emailCategory`
+ filter on `templates.list`, and `campaigns.create`. The enum member `HEADLESS`
+ is now `SELF_MANAGED_UNSUBSCRIBE`; `MARKETING` and `TRANSACTIONAL` are
+ unchanged.
+- **`data` → `payload`** in the body of `events.record` (the v1 write). The
+ legacy `events.track` is untouched and still takes `data` — the two endpoints
+ were renamed on different schedules, and this SDK reports what each one
+ actually accepts rather than papering over the difference.
+- **`mailFromStatus` → `mailFromDomainStatus`** on a domain, and
+ `mail_from_domain_status` on the v1 document.
+- **`emails.get` returns a different body** — the one change here worth reading
+ in full; see below.
+- **`EmailGetResponse` is gone.** It named the operation rather than the shape,
+ and was then reused by an operation that is not a GET. It is now two types:
+ `EmailResponse` (a single email) and `EmailDetailResponse` (an email plus its
+ delivery events), and `emails.get` resolves the latter.
+- **The double-opt-in confirmation route moved** from `/api/lists/confirm` to
+ `/api/lists/confirm-subscription`. Sendly has never sent that email for you, so
+ if you build the URL yourself — and `lists.subscribe` is documented on the
+ assumption that you do — change the path.
+
+### `emails.get`, specifically
+
+It used to hand back the whole database row together with an `events` array that
+was the **wrong relation**: the custom analytics events a caller records with
+`events.record`, not the delivery history the operation has always promised.
+
+It now returns an explicit field list plus `events` as the delivery timeline
+(`EmailEvent[]`, oldest first), and it fills `to` from the joined contact — which
+the spec had always declared and the response had never carried.
+
+Fields that used to leak out of it and no longer do: `bodyHash`, `dedupKey`,
+`idempotencyKey`, `linkMap`, `sesMessageId`, `sesInboundMessageId`, `body` and
+`headers`. Four of those are ledger keys for deduplication and idempotency; the
+rest are internal routing state or the rendered message itself. None of them were
+ever documented, and a caller reading them was reading Sendly's bookkeeping.
+
+If you were reading `events` from this call expecting custom events, read
+`events.list` instead. If you were reading the message body back out of it, keep
+your own copy — it is not published here.
+
+### Engagement left the delivery status
+
+`OPENED`, `CLICKED` and `COMPLAINED` are no longer delivery statuses on the
+platform, and the SDK's own status type — the enum behind `email.status` and the
+`status` filter on `emails.list` — no longer offers them. A message is
+`PENDING`, `SENDING`, `SENT`, `DELIVERED`, `RECEIVED`, `BOUNCED`, `FAILED`,
+`REJECTED`, `RENDERING_FAILURE`, `DELIVERY_DELAY` or `CANCELLED`. Engagement is a
+separate axis, read from `openedAt` / `clickedAt` / `complainedAt` and the
+`opens` / `clicks` counters on the email itself:
+
+```ts
+const { data: email } = await sendly.emails.get(id);
+const delivered = email.status === "DELIVERED"; // a delivery fact
+const engaged = email.openedAt !== null || email.clicks > 0; // an engagement fact
+```
+
+The two used to be one enum, which meant an opened message stopped reporting that
+it had been delivered.
+
## Upgrading from 0.x
**1.0 repoints `emails.send` to the versioned `POST /api/v1/emails`.** It now
@@ -105,6 +178,36 @@ const sendly = new Sendly({
});
```
+## The resources
+
+Every resource hangs off the client. A `V1` suffix means the method speaks the
+versioned dialect; an unsuffixed method on the same resource speaks the legacy
+one. See [Both dialects, one client](#both-dialects-one-client) for why both are
+here.
+
+| `sendly.*` | Methods |
+| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `emails` | `send`, `sendLegacy`, `sendTest`, `batch`, `list`, `get`, `cancelSchedule` |
+| `contacts` | `create`, `upsert`, `bulkCreate`, `bulkDelete`, `list`, `get`, `update`, `delete`, `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`, `deleteV1`, `topicPreferences` |
+| `lists` | `subscribe`, `unsubscribe`, `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`, `deleteV1`, `startValidationRun` |
+| `topics` | `list`, `listAll`, `create`, `get`, `update`, `setSubscription` |
+| `templates` | `create`, `list`, `get`, `update`, `delete`, `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`, `deleteV1` |
+| `snippets` | `create`, `list`, `get`, `update`, `delete` |
+| `domains` | `create`, `list`, `get`, `verify`, `getVerification`, `startSetup`, `assignStream`, `delete`, `listV1`, `listAllV1`, `createV1`, `getV1`, `verifyV1`, `deleteV1` |
+| `webhooks` | `create`, `list`, `get`, `update`, `delete`, `rotateSecret`, `listCalls`, `listV1`, `listAllV1`, `createV1`, `getV1`, `updateV1`, `deleteV1`, `rotateSecretV1` |
+| `suppression` | `add`, `list`, `get`, `remove`, `listV1`, `listAllV1`, `createV1`, `getV1`, `deleteV1` |
+| `events` | `track`, `record`, `list`, `listAll`, `listNames`, `stats` |
+| `campaigns` | `list`, `listAll`, `create`, `get`, `update`, `delete`, `send`, `cancel`, `pause`, `resume`, `stats`, `listFailures`, `listFailuresAll`, `retryFailed` |
+| `segments` | `list`, `listAll`, `create`, `get`, `update`, `delete`, `listContacts`, `listContactsAll` |
+| `workflows` | `list`, `listAll`, `create`, `get`, `update`, `delete`, `listExecutions`, `listExecutionsAll`, `startExecution`, `cancelExecution`, `stats`, `getGraph`, `replaceGraph`, `clone`, `pause`, `resume` |
+| `mailboxes` | `list`, `get`, `listAppPasswords`, `sendMessage`, `draftMessage` |
+| `validation` | `validateEmails`, `getRun`, `listResults`, `listResultsAll` |
+| `deliverability` | `diagnose`, `listDomainStats`, `listDomainStatsAll`, `listDmarcReports`, `listDmarcReportsAll` |
+| `analytics` | `timeseries`, `campaigns`, `topCampaigns` |
+| `usage` | `get` |
+| `projects` | `get` |
+| `verify` | `email` |
+
## Common operations
### Send a single email
@@ -144,18 +247,39 @@ console.log(
);
```
+### Read an email and its delivery history
+
+```ts
+const { data: email } = await sendly.emails.get(receipt.id);
+
+console.log(email.to, email.status, email.opens, email.clicks);
+
+// `events` is the DELIVERY timeline behind `status`, oldest first — not the
+// custom events you record with `events.record`, which are read from
+// `events.list`.
+for (const event of email.events) {
+ console.log(event.timestamp, event.status);
+}
+```
+
+This is one of the few legacy reads the SDK hands back enveloped rather than
+unwrapped, so the email is under `.data`.
+
### List emails with filters and cursor pagination
```ts
const page = await sendly.emails.list({ limit: 20, tag: "welcome", status: "DELIVERED" });
-for (const email of page.data.items) {
+for (const email of page.data) {
console.log(email.id, email.to, email.status);
}
-if (page.data.cursor) {
- const next = await sendly.emails.list({ limit: 20, cursor: page.data.cursor });
+if (page.nextCursor) {
+ const next = await sendly.emails.list({ limit: 20, cursor: page.nextCursor });
}
```
+`status` filters on the delivery lifecycle only. To find the messages somebody
+opened, read `openedAt` / `opens` on the rows — engagement is not a status.
+
### Upsert a contact
```ts
@@ -165,18 +289,64 @@ const contact = await sendly.contacts.upsert({
});
```
+The v1 half of the resource manages the same contacts with snake_case bodies and
+cursor pagination — `contacts.listV1`, `createV1`, `getV1`, `updateV1`,
+`deleteV1`, and `contacts.listAllV1` to walk every page:
+
+```ts
+// `subscribed` is the string "true" / "false" here, not a boolean — it is a
+// query parameter with three states, and omitting it means "both".
+for await (const contact of sendly.contacts.listAllV1({ subscribed: "true" })) {
+ console.log(contact.email, contact.custom_fields);
+}
+```
+
+Two things about `contacts.updateV1` catch people out: `email` is not patchable
+at all (an address is the contact's identity, and rewriting it in place would
+change who every earlier send was addressed to), and `custom_fields` is
+**replaced, not merged** — send back every key you mean to keep.
+
+### Read one contact's consent
+
+```ts
+const prefs = await sendly.contacts.topicPreferences(contact.id);
+
+// `prefs.subscribed` is the global marketing opt-out and OUTRANKS every topic:
+// false means nothing marketing reaches them whatever the rows below say.
+for (const topic of prefs.topics) {
+ console.log(topic.key, topic.subscribed, topic.pending);
+}
+```
+
+Each topic's `subscribed` is the effective answer the send path reaches today,
+with the topic's `default_opt_in` already folded in, so a contact who has never
+answered still reads correctly.
+
### Manage domains
```ts
const domain = await sendly.domains.create({ domain: "mail.your-domain.com" });
+// Publish each token as a CNAME record before verification can succeed.
+console.log(domain.dkimTokens);
+
await sendly.domains.verify(domain.id);
+
const status = await sendly.domains.getVerification(domain.id);
+// One status per record type, not one verdict for the domain.
+console.log(status.dkimStatus, status.spfStatus, status.dmarcStatus);
```
Pass `region` to pin the domain to an SES region (`us-east-1`, `us-west-2` or
`eu-west-1`). The first domain locks the project's region; later ones must match
it.
+A domain reports each DNS record type separately — `dkimStatus`, `spfStatus` and
+`dmarcStatus` are each `NOT_CHECKED`, `PENDING`, `VERIFIED` or `FAILED`, and
+`lastHealthCheckAt` says when they were last filled. `status` on the verification
+response is a different thing: SES's own raw DKIM state (`Success`, `Pending`),
+which is why both are published rather than collapsed into one. `receivingEnabled`
+says whether inbound mail for the domain is routed to Sendly mailboxes.
+
Publishing the DNS records by hand is not the only route. `startSetup` opens the
guided hand-off and returns the session exactly as the API returns it:
@@ -190,10 +360,27 @@ Nothing here is reshaped, because finishing setup means a **person** opening
`connectUrl` and authorising the change at their registrar. The SDK's job is to
hand back the link, not to model the flow behind it.
-### Read mailboxes
+`assignStream` points a verified identity at one kind of traffic:
+
+```ts
+await sendly.domains.assignStream(domain.id, {
+ stream: "TRANSACTIONAL",
+ streamDefault: true,
+ defaultFromAddress: "receipts@mail.your-domain.com",
+});
+```
-Receiving mailboxes on the project's verified domains. Reads only — see
-[What the SDK deliberately does not expose](#what-the-sdk-deliberately-does-not-expose).
+Streams are enforced, not labelled: once assigned, a send of the other kind from
+this identity is refused with `403` — which is what keeps a campaign's complaint
+rate off the identity your password resets go out on. `stream: null` unassigns
+it, returning it to carrying both. `streamDefault` demotes whichever identity
+currently holds the default for that stream, and `defaultFromAddress` has to be
+an address on this identity's own host.
+
+### Mailboxes: read, send, and draft
+
+Receiving mailboxes on the project's verified domains. The reads are reads; the
+two composition calls are not — `sendMessage` really sends.
```ts
const mailboxes = await sendly.mailboxes.list(); // not paginated
@@ -215,35 +402,316 @@ any of these reads; mailbox credentials are app passwords, created from the
dashboard and shown once. `listAppPasswords` returns only the passwords that are
still active — a revoked one drops out, so this is not an audit history.
-**The per-project cap is 10 mailboxes.** It counts only those holding, or
-mid-way to holding, a real account — `PROVISIONING`, `ACTIVE` and `SUSPENDED`.
-`FAILED` rows are excluded on purpose, so that a burst of failed provisions
-cannot eat a project's allowance and turn an outage into "you have reached your
-mailbox limit"; they are still returned by `list()`, so a project that has had
-failures can list more than 10. Exceeding the cap is a `409`
-(`SendlyConflictError`) from whatever creates the mailbox — which is not this
-SDK, since mailbox creation needs a signed-in user.
+**`sendMessage` sends real mail**, from the mailbox in the path, over its own
+domain, and the recipient can reply to it:
+
+```ts
+const sent = await sendly.mailboxes.sendMessage(mailbox.id, {
+ to: ["customer@example.com"],
+ subject: "Re: your order",
+ body: "Shipping tomorrow — tracking to follow.",
+});
+console.log(sent.conversationId, sent.messageId);
+```
+
+There is no `from` field, on purpose: a route that sends under a customer's own
+identity must not take that identity as an argument. `body` is plain text and
+HTML is refused — Sendly renders the HTML part itself, escaping as it goes, so
+text becomes markup in exactly one place. Bcc recipients are delivered to but
+appear in no header, so the copy filed in the Sent folder does not record them.
+Refusals worth handling by name: `422 RECIPIENT_SUPPRESSED`,
+`422 CONTENT_REFUSED`, and `503 CONTENT_SCAN_UNAVAILABLE` (no verdict yet for a
+young project — nothing was sent, retry shortly). A mailbox may send 60 messages
+an hour here.
+
+**`draftMessage` sends nothing.** It asks Sendly's assistant to write text and
+hands it back for you to review:
+
+```ts
+const draft = await sendly.mailboxes.draftMessage(mailbox.id, {
+ mode: "draft", // or "rewrite", or "subject"
+ brief: "Tell the customer their order ships tomorrow and apologise for the delay.",
+ tone: "apologetic",
+});
+console.log(draft.subject, draft.body, draft.sent); // sent is always false
+```
+
+`sent: false` is reported rather than assumed, so a draft cannot be mistaken for
+a send. It stores nothing, reads no correspondence, and needs only
+`mailboxes:read` where sending needs `mailboxes:send` — a client that may draft
+is not thereby a client that may mail your customers. Everything you pass is
+treated strictly as data describing what to write, never as instructions to the
+model. Capped at 120 requests an hour per project; `502` means the model was
+unreachable.
+
+### Templates and snippets
+
+```ts
+const template = await sendly.templates.create({
+ name: "Welcome",
+ subject: "Welcome to Acme",
+ body: "
Hi {{ name }}
{{> footer }}",
+ from: "hello@your-domain.com",
+ emailCategory: "MARKETING", // was `type` before 1.1
+});
+```
+
+`emailCategory` is `MARKETING`, `TRANSACTIONAL` or `SELF_MANAGED_UNSUBSCRIBE`
+(the member that used to be called `HEADLESS`). It defaults to `MARKETING` and
+is also the legacy list filter: `templates.list({ emailCategory: "MARKETING" })`.
+
+A template carries `currentVersion`, a counter an update increments only when it
+changes the **rendered content** — a rename leaves it alone. A campaign records
+the version it sent, so comparing the two is how you tell "the template changed
+since this went out" from "somebody retitled it".
+
+A **snippet** is a reusable fragment a template pulls in with `{{> name}}`.
+`name` is the literal identifier templates include, unique within the project, so
+a clash answers `409`:
+
+```ts
+await sendly.snippets.create({
+ name: "footer",
+ description: "Address block and unsubscribe line",
+ body: "
Acme Inc, 1 Example Way
",
+});
+
+const page = await sendly.snippets.list({ limit: 25, search: "footer" });
+console.log(page.data.data.length, page.data.hasMore);
+```
+
+Snippets are gated by the same `templates:*` scopes as the templates that include
+them, because a snippet is part of a template body rather than a resource with an
+audience of its own. Deleting one does not break the templates that include it —
+an absent snippet renders as an empty string, like an absent variable.
+
+### Consent: topics
+
+A topic is the subject a project mails about — a contact subscribes to a topic
+rather than to a campaign, so switching one off silences a whole audience.
+
+```ts
+const topic = await sendly.topics.create({
+ key: "product-updates", // stable; survives a rename of `name`, and is not patchable
+ name: "Product updates",
+ default_opt_in: true,
+});
+
+const result = await sendly.topics.setSubscription(topic.id, {
+ contact_id: contact.id,
+ subscribed: true,
+});
+```
+
+**Subscribing somebody through the API does not bypass confirmation.**
+`subscribed: true` parks the contact at `pending` and answers a
+`confirmation_url`; nothing is mailed on this topic until someone opens that
+link, and there is no parameter to skip it — a subscription a caller asserts is
+not evidence the mailbox holder agreed. Sendly does not send that email; **your
+application** delivers `result.confirmation_url`, from your own verified domain.
+`subscribed: false` records the opt-out immediately.
+
+`default_opt_in` decides what silence means for a contact who never answers: true
+for a topic introduced over a list that already consented to hear from you, false
+for anything a person has to ask for.
+
+There is no `topics.delete`. A topic is where people's answers are recorded, so
+deleting it would delete the choices they made; `update(id, { archived: true })`
+is the retire button and drops it from the preference centre and from new sends
+while every opt-out survives. `list({ include_archived: true })` brings them back.
+
+### Validate addresses before you mail them
+
+**Every address checked is billed.** Looping this over a contact list is looping
+over your invoice.
+
+```ts
+const batch = await sendly.validation.validateEmails({
+ emails: ["user@example.com", "typo@exmaple.com"], // at most 50 per call
+});
+
+for (const result of batch.results) {
+ // Branch on `verdict`, never on the flags: `is_personal` (Gmail, Outlook) and
+ // `is_role_address` (`support@`) describe ordinary, deliverable addresses.
+ console.log(result.email, result.verdict);
+}
+```
+
+The 50-address ceiling is a latency bound, not a payload one: every distinct
+domain in the batch costs a DNS round trip. To check a whole list, start the
+background run instead — one call, then poll:
+
+```ts
+const run = await sendly.lists.startValidationRun(list.id);
+
+const progress = await sendly.validation.getRun(run.id);
+// Finished when `status` is "completed" or "failed" — never when a percentage
+// reaches 100, because there is deliberately no total to divide by: a list
+// changes size while a run walks it.
+console.log(progress.status, progress.processed_count, progress.undeliverable_count);
+
+for await (const result of sendly.validation.listResultsAll(run.id, { verdict: "undeliverable" })) {
+ console.log(result.email, result.contact_id, result.reasons);
+}
+```
+
+A verdict of `unknown` is deliberately a separate value from `undeliverable`: it
+means DNS did not answer in time, so that address was **not checked**. Deleting a
+contact on `unknown` deletes a live one over a network hiccup. `undeliverable` is
+the page to read before acting on a run; `unknown` is the one never to act on.
+
+### Diagnose deliverability
+
+```ts
+const diagnosis = await sendly.deliverability.diagnose({
+ domain: "mail.your-domain.com", // required — this endpoint answers about one domain
+ address: "user@example.com", // optional RECIPIENT to check alongside it
+ window_days: 7,
+});
+
+// `findings` is worst first, and an empty array means nothing here explains a
+// delivery problem. Branch on a finding's `code`, never on its prose.
+for (const finding of diagnosis.findings) {
+ console.log(finding.severity, finding.code);
+}
+```
+
+Nothing there is looked up live: the DNS statuses are the verification refresh
+job's cached results, and `identity.last_checked_at` says when they were filled.
+`recent_delivery` is project-wide rather than per-domain — its own `scope` field
+says so — because an email row records no sending domain.
+
+`listDomainStats` is the axis `diagnose` cannot report: outcomes broken out by
+**recipient** domain and UTC day. These are the domains you send **to** —
+`gmail.com`, `outlook.com` — not the domains you send from, and they are how you
+catch one provider refusing nearly everything while the rest of your mail is
+healthy.
+
+```ts
+for await (const row of sendly.deliverability.listDomainStatsAll({ limit: 100 })) {
+ console.log(row.day, row.domain, row.delivered, row.bounced, row.computed_at);
+}
+```
+
+The counts come from an hourly rollup over a rolling 30-day window, not from a
+query run on request; each row's `computed_at` says when it was last rebuilt. No
+rate is published, because a rate over three sends is not information.
+
+`listDmarcReports` returns the DMARC aggregate (RUA) reports receiving providers
+have sent about your domains. **An empty list is the correct answer, not a bug**,
+until a policy domain is registered in this project and its DMARC record names an
+address we receive — and receivers send on their own schedule, typically once a
+day.
+
+```ts
+const reports = await sendly.deliverability.listDmarcReports({ limit: 20 });
+
+// Which kind of empty is this? `false` means no intake mailbox exists, so no
+// report can ever arrive — the feature is off, your domains are not "clean".
+if (!reports.intake_configured) {
+ console.warn("DMARC report intake is not configured on this deployment");
+}
+
+for (const report of reports.data) {
+ console.log(report.org_name, report.policy_domain, report.pass_count, report.fail_count);
+}
+```
+
+`intake_configured` exists because the two empty lists are otherwise
+indistinguishable, and reporting "no DMARC failures" off a feature that was
+never switched on is the worse of the two mistakes. Read the flag before you
+tell anyone the domains are healthy.
+
+`pass_count` counts DMARC **alignment** taken from `policy_evaluated`, not raw
+authentication results — a message can pass SPF for a domain that is not the one
+in its From header, which is exactly the case DMARC exists to catch.
### Subscribe a webhook
```ts
-const { webhook, secret } = await sendly.webhooks.create({
+const created = await sendly.webhooks.create({
url: "https://your-app.com/webhooks/sendly",
eventTypes: ["email.delivered", "email.bounced", "email.complained"],
});
-// store `secret` securely — used to verify HMAC signatures on incoming calls
+// store `created.data.secret` securely — used to verify HMAC signatures.
+// The endpoint is beside it rather than spread around it: `created.data.webhook.id`.
+```
+
+A webhook record carries `domains` — the sending domains this endpoint is scoped
+to, where an empty array means every domain on the project — and, while a
+rotation is in flight, `previousSecretExpiresAt`. A record never carries a
+secret or any fragment of one.
+
+On v1 the same registration resolves the secret beside the webhook, and adds
+rotation:
+
+```ts
+const { webhook, secret } = await sendly.webhooks.createV1({
+ url: "https://your-app.com/webhooks/sendly",
+ event_types: ["email.delivered", "email.bounced"],
+});
+
+const rotated = await sendly.webhooks.rotateSecretV1(webhook.id);
+console.log(rotated.secret, rotated.previous_secret_expires_at);
```
+`createV1` and `rotateSecretV1` are the only two responses that ever carry a
+signing secret; no read endpoint hands it back, so a secret you lose is replaced
+by rotating rather than recovered. The outgoing secret is not cut off at once —
+it keeps verifying until `previous_secret_expires_at`, and every delivery inside
+that window carries **both** signatures, so a verifier can be redeployed without
+dropping an event.
+
### Add to the suppression list
```ts
await sendly.suppression.add({ email: "angry@example.com", reason: "MANUAL" });
+
+// Alone among the legacy reads, this one answers no `{ success, data }`
+// envelope — the page IS the body.
+const page = await sendly.suppression.list({ reason: "MANUAL", limit: 100 });
+for (const record of page.items) {
+ console.log(record.email, record.reason, record.scope);
+}
```
+`scope` is `PROJECT` on every record this API creates or returns today; `GLOBAL`
+is reserved for a platform-wide block recorded outside your project.
+
+The v1 half addresses a record by the **address itself** and answers definitively
+either way — `200` means suppressed and says why, `404 resource_not_found` means
+it is not on the list. That is the difference from the legacy `suppression.get`,
+which answers `200 { suppressed: false }` for an address nobody suppressed:
+
+```ts
+import { SendlyNotFoundError } from "sendly-sdk";
+
+try {
+ const record = await sendly.suppression.getV1("angry@example.com");
+ console.log("suppressed:", record.reason, record.source);
+} catch (err) {
+ if (err instanceof SendlyNotFoundError) {
+ // not suppressed — mail may flow
+ } else throw err;
+}
+```
+
+Suppressing is idempotent and the first `reason` wins: an already-suppressed
+address answers with the existing record, so a later manual entry cannot
+overwrite what an SES bounce recorded. `source` is not accepted in the body — it
+is derived from the credential, so a record's provenance cannot be dressed up as
+a deliverability fact.
+
+`deleteV1` is the one call on this surface that can put mail back into an inbox
+that asked you to stop, and it does **not** clear AWS SES's own account-level
+suppression list: an address SES suppressed after a hard bounce stays
+undeliverable through SES even once this record is gone.
+
### Track a custom event
-Records a custom event against a contact. Works with both `sk_*` and `pk_*`
-keys (reserved system event names are rejected).
+Records a custom event against a contact. The legacy `events.track` works with
+both `sk_*` and `pk_*` keys (reserved system event names are rejected) and
+carries its payload in `data`:
```ts
const tracked = await sendly.events.track({
@@ -254,6 +722,20 @@ const tracked = await sendly.events.track({
console.log(tracked.contact, tracked.event);
```
+`events.record` is the same capability on `/api/v1/events`, and its payload field
+is called `payload`:
+
+```ts
+const event = await sendly.events.record({
+ name: "purchase.completed",
+ contact_id: contact.id, // must already exist — this endpoint never creates contacts
+ payload: { plan: "pro", amount: 4900 },
+});
+```
+
+New integrations should prefer `record`, which also unlocks `events.list`,
+`events.listNames` and `events.stats`.
+
### Verify an email address
```ts
@@ -263,11 +745,17 @@ if (!check.valid) {
}
```
+This is the free single-address syntax/MX check. It is not
+`validation.validateEmails`, which is the billed batch check with a verdict
+vocabulary behind it.
+
## The `/api/v1` surface
-Campaigns, segments, workflows, analytics, usage, and events live on Sendly's
-versioned API. They hang off the same client and the same base URL, but they
-speak a different dialect from the `/api/*` resources above:
+Campaigns, segments, workflows, analytics, usage, topics, validation,
+deliverability and events live on Sendly's versioned API, as does the `V1` half
+of contacts, lists, templates, domains, webhooks and suppression. They hang off
+the same client and the same base URL, but they speak a different dialect from
+the `/api/*` resources above:
- **Responses are the bare resource**, not a `{ success, data }` envelope, and
fields are `snake_case`.
@@ -296,12 +784,36 @@ const stats = await sendly.campaigns.stats(campaign.id);
console.log(stats.delivered, stats.open_rate);
```
+### Both dialects, one client
+
+Six resources — contacts, lists, templates, domains, webhooks and suppression —
+now answer on both surfaces, so their v1 methods carry a `V1` suffix:
+`contacts.list` is the legacy one, `contacts.listV1` the versioned one.
+
+The suffix is not decoration. The two methods answer the same question with
+different envelopes, different field cases and different error bodies, and a call
+site that mixes them up reads a `data` that is not there:
+
+```ts
+const legacy = await sendly.contacts.list({ limit: 20 });
+legacy.data.data; // Contact[] — inside the `{ success, data }` envelope
+legacy.data.nextCursor; // camelCase
+
+const v1 = await sendly.contacts.listV1({ limit: 20 });
+v1.data; // ContactV1[] — the bare body IS the list envelope
+v1.next_cursor; // snake_case
+```
+
+Legacy methods keep working and nothing about them changed in 1.1. New code
+should reach for the `V1` ones: they are the surface the contract is versioned
+against, and they carry `request_id` on every failure.
+
### Pagination
Every v1 list takes `limit` (1–100, default 20) and `after` (an opaque cursor
-from the previous response's `next_cursor`). Page manually, or let the SDK do
-it — each list has a companion `*All` async generator that walks the pages and
-yields individual items:
+from the previous response's `next_cursor`). Page manually, or let the SDK do it —
+each list has a companion `*All` async generator that walks the pages and yields
+individual items:
```ts
// Manual: stop when has_more goes false.
@@ -310,24 +822,121 @@ while (page.has_more && page.next_cursor) {
page = await sendly.campaigns.list({ limit: 50, after: page.next_cursor });
}
-// Automatic: campaigns.listAll, segments.listAll, segments.listContactsAll,
-// workflows.listAll, workflows.listExecutionsAll, events.listAll.
+// Automatic:
for await (const campaign of sendly.campaigns.listAll({ limit: 50 })) {
console.log(campaign.id, campaign.status);
}
```
+The seventeen companions: `campaigns.listAll`, `campaigns.listFailuresAll`,
+`contacts.listAllV1`, `deliverability.listDmarcReportsAll`,
+`deliverability.listDomainStatsAll`, `domains.listAllV1`, `events.listAll`,
+`lists.listAllV1`, `segments.listAll`, `segments.listContactsAll`,
+`suppression.listAllV1`, `templates.listAllV1`, `topics.listAll`,
+`validation.listResultsAll`, `webhooks.listAllV1`, `workflows.listAll` and
+`workflows.listExecutionsAll`.
+
+Through 1.0 there were two dialects: `topics.list` and
+`validation.listResults` took `cursor` and answered `cursor` where every other
+v1 list took `after`. The platform collapsed that for 1.1, so there is one shape
+to learn and one to write. If you were driving either of those two by hand, pass
+`after` and read `next_cursor`.
+
Keep the filter and sort arguments **fixed for the whole walk** — the cursor
encodes them, and changing them mid-pagination is answered with
`422 validation_error` telling you to restart from the first page. There is
-deliberately no total count.
+deliberately no total count. The one exception is `campaigns.listFailures`, which
+also carries `total`, because `retryFailed` acts on that number and `has_more`
+alone cannot tell you whether 3 or 30,000 sends failed.
+
+### Campaigns: who did not get it, and re-driving them
+
+`stats` says how many sends failed; only `listFailures` says who.
+
+```ts
+const failures = await sendly.campaigns.listFailures(campaign.id, { limit: 100 });
+console.log(failures.total, "recipients did not receive it");
+
+for await (const failure of sendly.campaigns.listFailuresAll(campaign.id)) {
+ console.log(failure.email, failure.reason, failure.failed_at);
+}
+
+const retry = await sendly.campaigns.retryFailed(campaign.id);
+console.log("re-queued", retry.queued);
+```
+
+`reason` comes from a fixed vocabulary rather than the underlying error text, so
+it is stable enough to branch on; it is `null` on rows recorded before reasons
+were captured.
+
+`retryFailed` re-drives **only** the recipients whose send failed — nobody who
+already received the campaign is mailed a second time, because each ledger row is
+claimed before it is touched and a row whose email exists already is re-queued
+rather than re-sent. The walk runs in the background, so the call resolves as
+soon as it is queued, reporting how many failed rows it was started for. Only a
+`SENT` campaign qualifies; a retry already running answers `409 conflict`.
+
+### Workflows: the graph, and the lifecycle
+
+`getGraph` returns every step — including the `TRIGGER` entry node — plus the
+directed transitions between them, and that body is accepted verbatim by
+`replaceGraph`:
+
+```ts
+const graph = await sendly.workflows.getGraph(workflow.id);
+graph.steps[0].config; // stored exactly as authored, camelCase keys and all
+
+const updated = await sendly.workflows.replaceGraph(workflow.id, {
+ steps: graph.steps,
+ transitions: graph.transitions,
+});
+```
+
+`replaceGraph` is a **PUT**, and that is the point: a graph is nodes _plus_ the
+edges between them, so a partial edit to a step list has no meaning without the
+transitions that reference it — half-applied, it would leave steps pointing at
+steps that no longer exist. Ids decide the outcome per step: one you send is
+updated in place, a fresh uuid creates a step, and an id you omit deletes that
+step _and its run history_. Exactly one step must be a `TRIGGER`, every
+transition must name steps in the same document, and no step may point at itself.
+It is refused with `409 conflict` while the workflow has running executions —
+those runs are standing on the steps being replaced.
+
+`clone` copies a workflow and its whole graph. The copy is **always created
+disabled**, whatever the original was: a clone exists to be reviewed, and one
+that started live would match the same trigger events as its original from the
+moment it appeared.
+
+```ts
+const copy = await sendly.workflows.clone(workflow.id, { name: "Welcome (v2 test)" });
+```
+
+`pause` and `resume` are deliberately asymmetric:
+
+```ts
+const paused = await sendly.workflows.pause(workflow.id);
+console.log("cancelled", paused.cancelled_executions, "in-flight runs");
+
+const resumed = await sendly.workflows.resume(workflow.id);
+console.log(resumed.cancelled_executions); // always 0
+```
+
+**Pausing cancels every `RUNNING`/`WAITING` execution** and reports how many —
+that is what separates it from `update(id, { enabled: false })`, which only stops
+new runs starting and leaves every in-flight contact walking the graph, next
+delay still expiring, next email still sending. **Resuming re-opens the workflow
+to new runs and does not restore the cancelled ones.** The cancellation is
+terminal; there is no undo, so pause when you mean to stop the sends already in
+flight and disable when you only mean to close the door. `resume` is refused with
+`422 validation_error` while any step is still unconfigured.
### Events: `track` vs `record`
-`events.track` is the legacy `POST /api/track` endpoint and is unchanged.
-`events.record` is the same capability on `/api/v1/events` — a different name
-only because `track` was taken. New integrations should prefer `record`, which
-also unlocks `events.list`, `events.listNames`, and `events.stats`.
+`events.track` is the legacy `POST /api/track` endpoint and is unchanged — its
+payload field is still `data`. `events.record` is the same capability on
+`/api/v1/events`, named differently only because `track` was taken, and its
+payload field is `payload`. New integrations should prefer `record`, which also
+unlocks `events.list`, `events.listNames`, and `events.stats`.
### Emails: `send` vs `sendLegacy`
@@ -395,9 +1004,11 @@ declarations, in both directions. They are: creating and deleting a mailbox,
creating and revoking an app password, all four API-key operations, and
creating a project. Use the dashboard or an OAuth connection for those.
-Mailbox **reads** are exposed (`mailboxes.list`, `mailboxes.get`,
-`mailboxes.listAppPasswords`) — their membership check is conditional, so a key
-really can call them.
+Mailbox **lifecycle** is what stays out of reach — not the mailbox resource as a
+whole. The three reads (`mailboxes.list`, `mailboxes.get`,
+`mailboxes.listAppPasswords`) have a conditional membership check, and
+`mailboxes.sendMessage` / `mailboxes.draftMessage` publish `ApiKeyAuth` outright,
+so a key really can call all five.
## Error handling
@@ -478,6 +1089,10 @@ hint, note that `X-RateLimit-Reset` is an **absolute** epoch-seconds instant
whereas the draft-11 `RateLimit` header's `t=` is **delta** seconds. The SDK
does not retry on your behalf.
+Note that `404 resource_not_found` is an ordinary answer from
+`suppression.getV1`, not a failure: it is how that route says "this address is
+not suppressed". Catch it rather than logging it.
+
### Legacy `/api/*` errors
Invalid input is reported as `SendlyValidationError`. The API returns **422**
@@ -505,10 +1120,17 @@ Pass `idempotencyKey` on any write that supports it — `emails.send`,
retries safe. Replays within 24 hours return the original result instead of
acting twice.
-Two v1 writes deliberately take no key. `events.record` is append-only and
-high-volume. `emails.sendTest` reaches only the caller's own inbox, a daily
-cap already bounds it, and "send me another one" is the normal second call
-rather than a mistake worth deduplicating.
+Nothing added in 1.1 takes a key. The v1 creates (`contacts.createV1`,
+`lists.createV1`, `templates.createV1`, `domains.createV1`,
+`webhooks.createV1`, `suppression.createV1`, `topics.create`,
+`snippets.create`) are all either naturally idempotent on their own key or cheap
+to repeat, and `campaigns.retryFailed` is guarded by a `409` on a retry already
+running rather than by a replay ledger.
+
+Two v1 writes deliberately take no key for reasons of their own. `events.record`
+is append-only and high-volume. `emails.sendTest` reaches only the caller's own
+inbox, a daily cap already bounds it, and "send me another one" is the normal
+second call rather than a mistake worth deduplicating.
```ts
await sendly.emails.send({ from, to, subject, body }, { idempotencyKey: `signup-${userId}` });
diff --git a/dist/index.cjs b/dist/index.cjs
index 38e9924..579201b 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -25,6 +25,7 @@ __export(index_exports, {
ContactsResource: () => ContactsResource,
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
DEFAULT_TOLERANCE_MS: () => DEFAULT_TOLERANCE_MS,
+ DeliverabilityResource: () => DeliverabilityResource,
DomainsResource: () => DomainsResource,
EmailsResource: () => EmailsResource,
EventsResource: () => EventsResource,
@@ -43,9 +44,12 @@ __export(index_exports, {
SendlyRateLimitError: () => SendlyRateLimitError,
SendlyServerError: () => SendlyServerError,
SendlyValidationError: () => SendlyValidationError,
+ SnippetsResource: () => SnippetsResource,
SuppressionResource: () => SuppressionResource,
TemplatesResource: () => TemplatesResource,
+ TopicsResource: () => TopicsResource,
UsageResource: () => UsageResource,
+ ValidationResource: () => ValidationResource,
VerifyResource: () => VerifyResource,
WebhooksResource: () => WebhooksResource,
WorkflowsResource: () => WorkflowsResource,
@@ -211,6 +215,45 @@ var CampaignsResource = class {
path: `/api/v1/campaigns/${encodeURIComponent(id)}/stats`
});
}
+ /**
+ * The recipients this campaign did not reach, and why.
+ *
+ * {@link stats} says how many sends failed; only this says who. `reason`
+ * comes from a fixed vocabulary rather than the underlying error text, so it
+ * is stable enough to branch on — and it is `null` on rows recorded before
+ * reasons were captured.
+ *
+ * Cursor-paginated like every other v1 list, but uniquely it also carries
+ * `total`: {@link retryFailed} acts on that number, and `has_more` alone
+ * cannot tell you whether 3 or 30,000 sends failed.
+ */
+ async listFailures(id, query) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/campaigns/${encodeURIComponent(id)}/failures`,
+ query
+ });
+ }
+ /** Iterate every failed send across pages, yielding one recipient at a time. */
+ async *listFailuresAll(id, query) {
+ yield* paginateCursor((after) => this.listFailures(id, { ...query, after }), query?.after);
+ }
+ /**
+ * Re-drive only the recipients whose send failed. Nobody who already received
+ * the campaign is mailed a second time — each ledger row is claimed before it
+ * is touched, and a row whose email exists already is re-queued, not re-sent.
+ *
+ * The walk runs in the background, so this resolves as soon as it is queued,
+ * reporting `queued`: how many failed rows it was started for. Only a `SENT`
+ * campaign qualifies (`400 validation_error` otherwise), and a retry already
+ * running answers `409 conflict`. Takes no body.
+ */
+ async retryFailed(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/campaigns/${encodeURIComponent(id)}/retry-failed`
+ });
+ }
};
// src/resources/contacts.ts
@@ -289,6 +332,180 @@ var ContactsResource = class {
noContent: true
});
}
+ /**
+ * List contacts on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after` with no total count, narrowed by
+ * `search` (case-insensitive substring on the address) and `subscribed`.
+ * Hold the filters steady for the whole walk — the cursor encodes them, and
+ * changing one mid-pagination returns `422 validation_error` asking you to
+ * restart. {@link listAllV1} drives the loop for you.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/contacts",
+ query
+ });
+ }
+ /** Iterate every v1 contact across pages, yielding one contact at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Create a contact. Only `email` is required — `subscribed` defaults to true
+ * server-side, and `custom_fields` is arbitrary JSON that templates can read
+ * back as `{{ variables }}`.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/contacts",
+ body
+ });
+ }
+ /**
+ * Retrieve a single contact by id. v1 has no lookup-by-address route — reach
+ * a contact you only know the email of through {@link listV1}'s `search`.
+ */
+ async getV1(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/contacts/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Patch a contact. Only the fields you send are changed, with two caveats.
+ *
+ * `email` is not patchable at all: an address is the contact's identity here,
+ * and rewriting it in place would change who every earlier send was addressed
+ * to. Create the new address instead.
+ *
+ * `custom_fields` is **replaced, not merged** — the object you send becomes
+ * the whole of it, so read the contact and send back every key you mean to
+ * keep. Sending a partial object silently drops the rest.
+ */
+ async updateV1(id, body) {
+ return this.client.request({
+ method: "PATCH",
+ path: `/api/v1/contacts/${encodeURIComponent(id)}`,
+ body
+ });
+ }
+ /**
+ * Delete a contact. Unlike the legacy {@link delete}, this resolves the
+ * `{ id, deleted }` acknowledgement rather than discarding it.
+ */
+ async deleteV1(id) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/contacts/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Read everything this contact has said about what they want.
+ *
+ * The top-level `subscribed` is the global marketing opt-out and outranks
+ * every topic: false means nothing marketing reaches them whatever the topic
+ * rows say. Each topic's own `subscribed` is the effective answer the send
+ * path reaches today, with the topic's `default_opt_in` already folded in, so
+ * a contact who has never answered still reads correctly.
+ */
+ async topicPreferences(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/contacts/${encodeURIComponent(id)}/topics`
+ });
+ }
+};
+
+// src/resources/deliverability.ts
+var DeliverabilityResource = class {
+ constructor(client) {
+ this.client = client;
+ }
+ client;
+ /**
+ * Diagnose one of your SENDING domains: its DNS identity, the project's
+ * recent delivery outcomes, optionally one recipient's suppression state, and
+ * the `findings` drawn from them, worst first. Branch on a finding's `code`,
+ * never on its prose.
+ *
+ * `query.domain` is required — the endpoint answers about one domain. The
+ * optional `address` is a RECIPIENT to check alongside it, because being
+ * suppressed is the single most common reason one person stops receiving mail
+ * while everyone else still does. `window_days` (1–30, default 7) only moves
+ * the delivery counters.
+ *
+ * Nothing here is looked up live: the DNS statuses are the verification
+ * refresh job's cached results, and `identity.last_checked_at` says when they
+ * were filled. `recent_delivery` is project-wide rather than per-domain — its
+ * own `scope` field says so — because an email row records no sending domain.
+ */
+ async diagnose(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/deliverability/diagnose",
+ query
+ });
+ }
+ /**
+ * Delivery outcomes broken out by RECIPIENT domain and UTC day, newest day
+ * first.
+ *
+ * These are the domains you send TO — `gmail.com`, `outlook.com` — not the
+ * domains you send FROM. That is the axis {@link diagnose} cannot report: its
+ * project-wide rates hide the case that matters most, one recipient domain
+ * refusing nearly everything while the rest of your mail is healthy.
+ *
+ * Cursor-paginated on `limit` + `after`. The counts come from an hourly
+ * rollup job over a rolling 30-day window, not from a query run on request;
+ * each row's `computed_at` says when it was last rebuilt. No rate is
+ * published, because a rate over three sends is not information.
+ */
+ async listDomainStats(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/deliverability/domains",
+ query
+ });
+ }
+ /** Iterate every recipient-domain row across pages, one day-and-domain at a time. */
+ async *listDomainStatsAll(query) {
+ yield* paginateCursor((after) => this.listDomainStats({ ...query, after }), query?.after);
+ }
+ /**
+ * DMARC aggregate (RUA) reports that receiving providers have sent about your
+ * domains, newest reporting window first. Cursor-paginated on `limit` +
+ * `after`.
+ *
+ * An empty list is the correct answer, not a bug, until a policy domain is
+ * registered in this project and its DMARC record names an address we
+ * receive: only reports about a registered domain are stored, and receivers
+ * send them on their own schedule (typically once a day).
+ *
+ * `intake_configured` says which kind of empty you are looking at. When it is
+ * `false` this deployment has no DMARC report intake mailbox at all, so no
+ * report can ever arrive and an empty `data` means the feature is off — not
+ * that your domains are clean. The two are otherwise indistinguishable, so
+ * read the flag before reporting "no DMARC failures" to anyone.
+ *
+ * `pass_count` counts DMARC ALIGNMENT taken from `policy_evaluated`, not raw
+ * authentication results — a message can pass SPF for a domain that is not
+ * the one in its From header, which is exactly the case DMARC exists to
+ * catch.
+ */
+ async listDmarcReports(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/deliverability/dmarc",
+ query
+ });
+ }
+ /** Iterate every DMARC report across pages, one report at a time. */
+ async *listDmarcReportsAll(query) {
+ yield* paginateCursor((after) => this.listDmarcReports({ ...query, after }), query?.after);
+ }
};
// src/resources/domains.ts
@@ -304,7 +521,10 @@ var DomainsResource = class {
* `eu-west-1`). On the very first domain for a project this also locks the
* project's region; subsequent calls must match.
*
- * The response includes DNS records to set.
+ * The response carries `dkimTokens` — the SES DKIM tokens to publish as
+ * CNAME records before the domain can verify — alongside `dkimStatus`,
+ * `spfStatus` and `dmarcStatus`, each the result of the last DNS check for
+ * that record type.
*/
async create(body) {
const envelope = await this.client.request({
@@ -329,7 +549,14 @@ var DomainsResource = class {
});
return this.client.unwrap(envelope);
}
- /** Trigger SES verification for a domain. */
+ /**
+ * Trigger SES verification for a domain.
+ *
+ * `status` is SES's own raw DKIM verification state (`Success`, `Pending`),
+ * while `dkimStatus`, `spfStatus` and `dmarcStatus` are this platform's own
+ * DNS check per record type. `tokens` carries the DKIM tokens SES has still
+ * to report and is absent once verification has resolved.
+ */
async verify(id) {
const envelope = await this.client.request({
method: "POST",
@@ -361,6 +588,29 @@ var DomainsResource = class {
});
return this.client.unwrap(envelope);
}
+ /**
+ * Assign this sending identity to transactional or marketing traffic.
+ *
+ * Streams are enforced, not labelled: once assigned, a send of the other kind
+ * from this identity is refused with 403 — which is what keeps a campaign's
+ * complaint rate off the identity your password resets go out on. Pass
+ * `stream: null` to unassign, returning it to carrying both.
+ *
+ * `streamDefault` demotes whichever identity currently holds the default for
+ * that stream, and `defaultFromAddress` has to be an address on this
+ * identity's own host. Every field is optional; an omitted one is left alone.
+ *
+ * Legacy dialect: camelCase body, and the updated domain arrives inside the
+ * `{ success, data }` envelope this method unwraps for you.
+ */
+ async assignStream(id, body) {
+ const envelope = await this.client.request({
+ method: "PATCH",
+ path: `/api/domains/${encodeURIComponent(id)}`,
+ body
+ });
+ return this.client.unwrap(envelope);
+ }
/** Delete a domain. */
async delete(id) {
await this.client.request({
@@ -368,6 +618,88 @@ var DomainsResource = class {
path: `/api/domains/${encodeURIComponent(id)}`
});
}
+ /**
+ * List sending domains, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. {@link listAllV1}
+ * drives the loop for you.
+ *
+ * `verified` is SES's verdict on the identity and is what decides whether mail
+ * can leave from this domain; `dkim_verified` is a separate fact — what the
+ * DNS health refresh last read for the DKIM records — so the two disagree
+ * while a re-check is in flight and neither is a spelling of the other.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/domains",
+ query
+ });
+ }
+ /** Iterate every sending domain across pages, yielding one domain at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Register a sending domain and start SES DKIM verification.
+ *
+ * The identity comes back with `verified: false` — nothing is verified until
+ * the DKIM records are published in the domain's own DNS and SES resolves
+ * them, so poll {@link verifyV1} after publishing them.
+ *
+ * The first domain a project adds LOCKS the project's SES `region`; every
+ * later domain must match it. `stream_default` requires `stream`, and sending
+ * it alone is answered with `422 validation_error` rather than ignored.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/domains",
+ body
+ });
+ }
+ /** Retrieve a single sending domain. */
+ async getV1(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/domains/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Re-read the domain's state from SES and DNS, and resolve the refreshed
+ * document.
+ *
+ * This does not verify anything and changes none of the domain's own fields.
+ * Verification happens in the domain's DNS, when its owner publishes the DKIM
+ * records SES minted at creation, and Amazon decides when those resolve. What
+ * this call does is ask SES what it currently sees, re-check SPF and DMARC,
+ * and persist that answer — so a caller polling after a DNS change learns the
+ * outcome without waiting for the periodic sweep. Calling it on a domain whose
+ * records are not published yet is not an error and does not hurry anything.
+ *
+ * A POST rather than a GET because the refreshed state is persisted and a
+ * verified/unverified transition notifies the project.
+ */
+ async verifyV1(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/domains/${encodeURIComponent(id)}/verify`
+ });
+ }
+ /**
+ * Remove a sending domain. Resolves `{ id, deleted }`.
+ *
+ * Refused with `409 conflict` while a template, workflow step or active
+ * campaign still sends from an address on this host. The SES identity goes
+ * too unless another project holds the same host — and its DKIM keys with it,
+ * so re-adding later mints records that must be published again.
+ */
+ async deleteV1(id) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/domains/${encodeURIComponent(id)}`
+ });
+ }
};
// src/resources/emails.ts
@@ -450,14 +782,28 @@ var EmailsResource = class {
query
});
}
- /** Fetch a single email and its delivery events. */
+ /**
+ * Fetch a single email together with its DELIVERY history, oldest first.
+ *
+ * `events` here is the delivery timeline behind `status` — not the custom
+ * events recorded with `events.record`, which are read from `events.list`.
+ * Before 1.1 this operation answered the wrong relation and published the
+ * message's dedup and idempotency ledger keys along with it.
+ */
async get(id) {
return this.client.request({
method: "GET",
path: `/api/emails/${encodeURIComponent(id)}`
});
}
- /** Cancel a scheduled (PENDING) email before it fires. */
+ /**
+ * Cancel a scheduled (PENDING) email before it fires.
+ *
+ * Resolves the email itself, not an empty acknowledgement: the contract has
+ * always published `EmailResponse` here, and the caller wants the row's new
+ * status more than it wants a `{ success: true }` it already inferred from the
+ * absence of an exception.
+ */
async cancelSchedule(id) {
return this.client.request({
method: "DELETE",
@@ -559,8 +905,8 @@ var ListsResource = class {
* **Double opt-in.** When the list has `doubleOptIn` enabled the membership
* is created as `PENDING` and the result carries a `confirmToken`. Sendly
* does **not** send the confirmation email — your application must deliver
- * `/api/lists/confirm?token=` to the contact itself. The token
- * is valid for 24 hours.
+ * `/api/lists/confirm-subscription?token=` to the contact
+ * itself. The token is valid for 24 hours.
*
* **Re-subscribing after an opt-out.** If the email already holds an
* `UNSUBSCRIBED` membership on this list, the call fails with
@@ -590,6 +936,83 @@ var ListsResource = class {
});
return this.client.unwrap(envelope);
}
+ /**
+ * List the project's subscriber lists on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold the
+ * arguments steady for the whole walk — changing them mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/lists",
+ query
+ });
+ }
+ /** Iterate every list across pages, yielding one list at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Create a list. Only `name` is required; `double_opt_in` defaults to false.
+ *
+ * Turning double opt-in on does not make Sendly send anything — it only
+ * changes {@link subscribe} to create the membership as `PENDING` and hand
+ * back the `confirmToken` your application delivers.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/lists",
+ body
+ });
+ }
+ /**
+ * Retrieve a single list. `member_count` counts memberships in *any* status,
+ * `PENDING` and `UNSUBSCRIBED` included, so it is not the size of the
+ * audience a campaign would reach.
+ */
+ async getV1(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/lists/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Patch a list's name, description, opt-in mode, confirmation template, or
+ * redirect URL. Only the fields you send are changed; `member_count` is
+ * derived and never accepted here.
+ */
+ async updateV1(id, body) {
+ return this.client.request({
+ method: "PATCH",
+ path: `/api/v1/lists/${encodeURIComponent(id)}`,
+ body
+ });
+ }
+ /** Delete a list. Resolves `{ id, deleted }`. Removes the list, not its contacts. */
+ async deleteV1(id) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/lists/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Start a bulk address-validation run over the list's members.
+ *
+ * **Billed per address checked**, so starting a run over a large list costs
+ * real money every time — it is not a free refresh. Answers `202` with the
+ * run in `pending`; read its progress and counts back with
+ * `validation.getRun`.
+ */
+ async startValidationRun(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/lists/${encodeURIComponent(id)}/validation-runs`
+ });
+ }
};
// src/resources/mailboxes.ts
@@ -647,6 +1070,57 @@ var MailboxesResource = class {
});
return this.client.unwrap(envelope);
}
+ /**
+ * SENDS a new message — real mail leaves the account, from the mailbox in the
+ * path, over its own domain, and the recipient can reply to it.
+ *
+ * There is no `from` field, on purpose: a route that sends under a customer's
+ * own identity must not take that identity as an argument. `body` is plain
+ * text and HTML is refused — Sendly renders the HTML part itself, escaping as
+ * it goes, so text becomes markup in exactly one place.
+ *
+ * Bcc recipients are delivered to but appear in no header, so the copy filed
+ * in the mailbox's Sent folder does not record them. The message is stored as
+ * a new conversation, and the reply threads onto it.
+ *
+ * Refusals worth handling by name: `422 RECIPIENT_SUPPRESSED` (a recipient is
+ * on the project's suppression list), `422 CONTENT_REFUSED` (the outbound
+ * scanner declined it), `503 CONTENT_SCAN_UNAVAILABLE` (no verdict yet for a
+ * young project — nothing was sent, retry shortly). A mailbox may send 60
+ * messages an hour here.
+ */
+ async sendMessage(id, body) {
+ const envelope = await this.client.request({
+ method: "POST",
+ path: `/api/mailboxes/${encodeURIComponent(id)}/messages`,
+ body
+ });
+ return this.client.unwrap(envelope);
+ }
+ /**
+ * SENDS NOTHING — asks Sendly's assistant to write text for this mailbox and
+ * hands it back for you to review. The response always reports `sent: false`,
+ * and no argument changes that.
+ *
+ * `mode` picks the job: `draft` writes a new email from a brief, `rewrite`
+ * reworks text you already have, `subject` returns alternative subject lines
+ * in `subjects`. The mailbox is named only so the text can be written in that
+ * address's voice; no correspondence is read and nothing is stored.
+ *
+ * That is why this asks only for `mailboxes:read` while {@link sendMessage}
+ * needs `mailboxes:send` — a client that may draft is not thereby a client
+ * that may mail your customers. Everything you pass is treated strictly as
+ * data describing what to write, never as instructions to the model. Capped
+ * at 120 requests an hour per project; `502` means the model was unreachable.
+ */
+ async draftMessage(id, body) {
+ const envelope = await this.client.request({
+ method: "POST",
+ path: `/api/mailboxes/${encodeURIComponent(id)}/drafts`,
+ body
+ });
+ return this.client.unwrap(envelope);
+ }
};
// src/resources/projects.ts
@@ -742,6 +1216,63 @@ var SegmentsResource = class {
}
};
+// src/resources/snippets.ts
+var SnippetsResource = class {
+ constructor(client) {
+ this.client = client;
+ }
+ client;
+ /**
+ * Create a snippet. `name` is the literal identifier templates include with
+ * `{{> name}}` and is unique within the project, so a clash answers 409.
+ */
+ async create(body) {
+ const envelope = await this.client.request({
+ method: "POST",
+ path: "/api/snippets",
+ body
+ });
+ return this.client.unwrap(envelope);
+ }
+ /** List snippets with cursor pagination (`limit`/`cursor`) + optional `search` over name and description. */
+ async list(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/snippets",
+ query
+ });
+ }
+ /** Fetch a single snippet by id. */
+ async get(id) {
+ const envelope = await this.client.request({
+ method: "GET",
+ path: `/api/snippets/${encodeURIComponent(id)}`
+ });
+ return this.client.unwrap(envelope);
+ }
+ /** Patch an existing snippet. */
+ async update(id, body) {
+ const envelope = await this.client.request({
+ method: "PATCH",
+ path: `/api/snippets/${encodeURIComponent(id)}`,
+ body
+ });
+ return this.client.unwrap(envelope);
+ }
+ /**
+ * Delete a snippet. The API answers 200 with `{ success, data: { id } }`; the
+ * SDK resolves void. Templates that still include it keep rendering — an
+ * absent snippet renders as an empty string, like an absent variable.
+ */
+ async delete(id) {
+ await this.client.request({
+ method: "DELETE",
+ path: `/api/snippets/${encodeURIComponent(id)}`,
+ noContent: true
+ });
+ }
+};
+
// src/resources/suppression.ts
var SuppressionResource = class {
constructor(client) {
@@ -757,7 +1288,14 @@ var SuppressionResource = class {
});
return this.client.unwrap(envelope);
}
- /** List suppressions with optional reason filter + cursor pagination. */
+ /**
+ * List suppressions with optional reason filter + cursor pagination.
+ *
+ * Alone among the legacy reads, this route answers no `{ success, data }`
+ * envelope: the page IS the body, `{ items, nextCursor }`, so nothing is
+ * unwrapped. Each record carries `scope` — `PROJECT` for every record this
+ * API creates or returns today.
+ */
async list(query) {
return this.client.request({
method: "GET",
@@ -780,6 +1318,70 @@ var SuppressionResource = class {
noContent: true
});
}
+ /**
+ * List suppressed addresses, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold `reason`
+ * steady for the whole walk — changing it mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/suppressions",
+ query
+ });
+ }
+ /** Iterate every suppressed address across pages, yielding one record at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Suppress an address, so no further send reaches it.
+ *
+ * Idempotent: an already-suppressed address answers `201` with the existing
+ * record, and the first `reason` wins — a later manual entry must not
+ * overwrite what an SES bounce recorded. `source` is not accepted in the
+ * body; it is derived from the credential, so a record's provenance cannot be
+ * dressed up as a deliverability fact.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/suppressions",
+ body
+ });
+ }
+ /**
+ * Retrieve the suppression record for one address.
+ *
+ * The answer is definite either way: `200` means suppressed and says why,
+ * `404 resource_not_found` means the address is not on the list. A `200` may
+ * also come from a platform-wide block recorded outside this project.
+ */
+ async getV1(email) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/suppressions/${encodeURIComponent(email)}`
+ });
+ }
+ /**
+ * Un-suppress an address: mail can flow to it again. Resolves
+ * `{ email, deleted }`.
+ *
+ * This is the one call on this surface that can put mail back into an inbox
+ * that asked you to stop. It does NOT clear AWS SES's own account-level
+ * suppression list, so an address SES suppressed after a hard bounce stays
+ * undeliverable through SES even once this record is gone. Idempotent: an
+ * address that was never suppressed answers `200` too.
+ */
+ async deleteV1(email) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/suppressions/${encodeURIComponent(email)}`
+ });
+ }
};
// src/resources/templates.ts
@@ -797,7 +1399,7 @@ var TemplatesResource = class {
});
return this.client.unwrap(envelope);
}
- /** List templates with cursor pagination (`limit`/`cursor`) + optional type filter. */
+ /** List templates with cursor pagination (`limit`/`cursor`) + optional `emailCategory` filter. */
async list(query) {
return this.client.request({
method: "GET",
@@ -813,7 +1415,14 @@ var TemplatesResource = class {
});
return this.client.unwrap(envelope);
}
- /** Patch an existing template. */
+ /**
+ * Patch an existing template.
+ *
+ * An update that changes the rendered content increments `currentVersion`;
+ * one that only renames leaves it alone. A campaign records the version it
+ * sent, so comparing the two is how a caller tells "the template changed
+ * since" from "the template was renamed".
+ */
async update(id, body) {
const envelope = await this.client.request({
method: "PATCH",
@@ -830,6 +1439,172 @@ var TemplatesResource = class {
noContent: true
});
}
+ /**
+ * List templates, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. `search` here
+ * matches the name only — narrower than the dashboard's search, which also
+ * reads description and subject. Hold `search` and `email_category` steady
+ * for the whole walk; changing either mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/templates",
+ query
+ });
+ }
+ /** Iterate every template across pages, yielding one template at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Create a template. `email_category` defaults to `MARKETING` server-side.
+ *
+ * The `from` domain must already be a verified sending identity — an
+ * unverified sender is refused with `403 forbidden` here rather than becoming
+ * a campaign that fails at send time.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/templates",
+ body
+ });
+ }
+ /** Retrieve a single template. */
+ async getV1(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/templates/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Patch a template. Only the fields you send are changed.
+ *
+ * Touching `subject`, `body`, `from`, `from_name` or `reply_to` snapshots the
+ * previous content into version history and increments `version`; touching
+ * only `name`, `description` or `email_category` does not, because neither is
+ * content a send would have rendered.
+ */
+ async updateV1(id, body) {
+ return this.client.request({
+ method: "PATCH",
+ path: `/api/v1/templates/${encodeURIComponent(id)}`,
+ body
+ });
+ }
+ /**
+ * Delete a template. Resolves `{ id, deleted }` — the legacy `delete` above
+ * discards that body, this one hands it back.
+ *
+ * A template a workflow step or an active campaign (DRAFT, SCHEDULED or
+ * SENDING) still points at is refused with `409 conflict`. Emails already
+ * sent from it are not erased.
+ */
+ async deleteV1(id) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/templates/${encodeURIComponent(id)}`
+ });
+ }
+};
+
+// src/resources/topics.ts
+var TopicsResource = class {
+ constructor(client) {
+ this.client = client;
+ }
+ client;
+ /**
+ * List topics, newest first.
+ *
+ * Archived topics are omitted unless `include_archived` asks for them. There
+ * is no delete — archiving is the retire button, because a topic is where
+ * people's answers are recorded. {@link listAll} drives the loop for you.
+ *
+ * Paginated on `limit` + `after`, like every other v1 collection.
+ */
+ async list(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/topics",
+ query
+ });
+ }
+ /**
+ * Iterate every topic across pages, yielding one topic at a time.
+ *
+ * This used to be written out by hand: the endpoint named its cursor `cursor`
+ * on both sides where every other v1 list takes `after` and answers
+ * `next_cursor`, so the shared walker sent a parameter the route ignored and
+ * read a field it never returned — which silently re-fetched page one until
+ * `has_more` happened to be false. The route speaks the one dialect now, so
+ * this delegates like every other collection.
+ */
+ async *listAll(query) {
+ yield* paginateCursor((after) => this.list({ ...query, after }), query?.after);
+ }
+ /**
+ * Create a topic.
+ *
+ * `key` is the stable name every preference form and integration refers to,
+ * so it survives a rename of `name` and cannot be changed afterwards.
+ *
+ * `default_opt_in` decides what silence means for a contact who never
+ * answers: true for a topic introduced over a list that already consented to
+ * hear from you, false for anything a person has to ask for.
+ */
+ async create(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/topics",
+ body
+ });
+ }
+ /** Retrieve a single topic. */
+ async get(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/topics/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Patch a topic. Only the fields you send are changed.
+ *
+ * `key` is not patchable, and `archived: true` stands in for the delete that
+ * does not exist: it drops the topic from the preference centre and from new
+ * sends while every opt-out recorded against it survives.
+ */
+ async update(id, body) {
+ return this.client.request({
+ method: "PATCH",
+ path: `/api/v1/topics/${encodeURIComponent(id)}`,
+ body
+ });
+ }
+ /**
+ * Record what one contact wants on one topic. The two directions are not
+ * symmetric, on purpose.
+ *
+ * `subscribed: true` does NOT subscribe anybody: it parks the contact at
+ * `pending` and answers a `confirmation_url`, and nothing is mailed on this
+ * topic until someone opens that link. There is no parameter to skip it —
+ * a caller asserting a subscription is not evidence the mailbox holder
+ * agreed. Sendly does not send the confirmation email; you do, from your own
+ * verified domain.
+ *
+ * `subscribed: false` records the opt-out immediately.
+ */
+ async setSubscription(id, body) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/topics/${encodeURIComponent(id)}/subscriptions`,
+ body
+ });
+ }
};
// src/resources/usage.ts
@@ -854,6 +1629,78 @@ var UsageResource = class {
}
};
+// src/resources/validation.ts
+var ValidationResource = class {
+ constructor(client) {
+ this.client = client;
+ }
+ client;
+ /**
+ * Check a batch of addresses. **This is billed per address checked** — every
+ * entry in `emails` costs money, so looping this over a contact list is
+ * looping over your invoice. Validate a whole list with the background run
+ * (`lists.startValidationRun`) instead of paging it through here.
+ *
+ * At most 50 addresses per call. That ceiling is a latency bound, not a
+ * payload one: every distinct domain in the batch costs a DNS round trip.
+ *
+ * Branch on each result's `verdict`, never on the flags — `is_personal`
+ * (Gmail, Outlook) and `is_role_address` (`support@`) describe ordinary,
+ * deliverable addresses that real customers use. A verdict of `unknown` means
+ * DNS did not answer in time, so that address was NOT checked; it is a
+ * separate value from `undeliverable` on purpose, and deleting a contact on
+ * `unknown` deletes a live one over a network hiccup.
+ */
+ async validateEmails(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/email-validations",
+ body
+ });
+ }
+ /**
+ * Retrieve a bulk validation run: how far it has got, and what it found.
+ *
+ * The other way a run starts is `lists.startValidationRun`, which validates
+ * every address on a list in the background and answers with the run this
+ * method polls. A run is finished when `status` is `completed` or `failed` —
+ * never when a percentage reaches 100, because there is deliberately no total
+ * to divide by: a list changes size while a run walks it.
+ */
+ async getRun(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/validation-runs/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * List one page of a run's verdicts. Filter with `verdict` — `undeliverable`
+ * is the page to read before acting on a run, and `unknown` is the one never
+ * to act on, since those addresses were not actually checked.
+ *
+ * Pages on `after` and answers `next_cursor`, like every other v1
+ * collection. {@link listResultsAll} drives that loop for you.
+ */
+ async listResults(id, query) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/validation-runs/${encodeURIComponent(id)}/results`,
+ query
+ });
+ }
+ /**
+ * Iterate every result across pages, yielding one address's verdict at a time.
+ *
+ * This was hand-rolled through 1.0, because the endpoint spoke `cursor` on
+ * both sides while the shared helper sends `after` and reads `next_cursor` —
+ * so routing it through the helper would have sent an ignored parameter and
+ * re-fetched page one forever. The route speaks the one dialect now.
+ */
+ async *listResultsAll(id, query) {
+ yield* paginateCursor((after) => this.listResults(id, { ...query, after }), query?.after);
+ }
+};
+
// src/resources/verify.ts
var VerifyResource = class {
constructor(client) {
@@ -885,6 +1732,10 @@ var WebhooksResource = class {
* Create a new outbound webhook subscription. The response includes the
* signing secret — store it now, it is only returned in full at creation
* and rotation time.
+ *
+ * `data` holds the two separately: `data.webhook` is the endpoint and
+ * `data.secret` is the plaintext. The endpoint's own fields are NOT spread
+ * alongside the secret, so the id is `data.webhook.id`.
*/
async create(body) {
return this.client.request({
@@ -938,6 +1789,99 @@ var WebhooksResource = class {
query
});
}
+ /**
+ * List webhook endpoints, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count.
+ * {@link listAllV1} drives the loop for you. Signing secrets are not on this
+ * response — see {@link rotateSecretV1} if you have lost one.
+ */
+ async listV1(query) {
+ return this.client.request({
+ method: "GET",
+ path: "/api/v1/webhooks",
+ query
+ });
+ }
+ /** Iterate every webhook endpoint across pages, yielding one endpoint at a time. */
+ async *listAllV1(query) {
+ yield* paginateCursor((after) => this.listV1({ ...query, after }), query?.after);
+ }
+ /**
+ * Register an endpoint to receive HMAC-signed deliveries for the events named
+ * in `event_types`.
+ *
+ * Resolves `{ webhook, secret }`, and this is one of only two calls that ever
+ * carry the signing secret — {@link rotateSecretV1} is the other. It is shown
+ * exactly once: no read endpoint returns it, so store it now, because a
+ * secret you lose is replaced by rotating rather than recovered. Feed it to
+ * `verifySignature` to authenticate the deliveries that arrive at your
+ * endpoint.
+ */
+ async createV1(body) {
+ return this.client.request({
+ method: "POST",
+ path: "/api/v1/webhooks",
+ body
+ });
+ }
+ /** Retrieve a single webhook endpoint. The signing secret is not on this response. */
+ async getV1(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/webhooks/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Patch a webhook endpoint. Omitted fields are left alone.
+ *
+ * `event_types` REPLACES the stored subscription list rather than merging
+ * into it, so an event you omit is unsubscribed. Setting `status` back to
+ * `ACTIVE` from `DISABLED` also clears the consecutive-failure counter, so an
+ * auto-disabled endpoint gets a clean slate. The signing secret is untouched
+ * by an update, and is not on this response.
+ */
+ async updateV1(id, body) {
+ return this.client.request({
+ method: "PATCH",
+ path: `/api/v1/webhooks/${encodeURIComponent(id)}`,
+ body
+ });
+ }
+ /**
+ * Delete a webhook endpoint, and its delivery history with it — a delivery
+ * attempt is a fact about this endpoint and means nothing once the endpoint is
+ * gone. Resolves `{ id, deleted }`. Deliveries already in flight are not
+ * recalled, so the endpoint may still receive an event shortly after this.
+ */
+ async deleteV1(id) {
+ return this.client.request({
+ method: "DELETE",
+ path: `/api/v1/webhooks/${encodeURIComponent(id)}`
+ });
+ }
+ /**
+ * Mint a fresh signing secret for an endpoint.
+ *
+ * The new plaintext is returned exactly once, here — this and
+ * {@link createV1} are the only two responses that ever carry the secret, and
+ * no read endpoint hands it back, so store it now and give it to
+ * `verifySignature`. A secret you lose is replaced by rotating again rather
+ * than recovered.
+ *
+ * The outgoing secret is not cut off at once: it keeps verifying until
+ * `previous_secret_expires_at`, and every delivery inside that window carries
+ * BOTH signatures, so a verifier can be redeployed without dropping an event.
+ * Past that moment the old secret starts being rejected — as does the older of
+ * two secrets if you rotate twice inside the window, because only one previous
+ * secret is ever live. `url`, `event_types` and `status` are unchanged.
+ */
+ async rotateSecretV1(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/webhooks/${encodeURIComponent(id)}/rotate-secret`
+ });
+ }
};
// src/resources/workflows.ts
@@ -1047,6 +1991,96 @@ var WorkflowsResource = class {
query
});
}
+ /**
+ * Every step in the workflow — including its `TRIGGER` entry node — plus the
+ * directed transitions between them.
+ *
+ * A step's `config` comes back exactly as stored, camelCase keys and all,
+ * rather than projected into the snake_case used elsewhere on v1: the same
+ * document is authored by the visual editor, and renaming its keys on the way
+ * out would silently drop any key this API does not know on the way back in.
+ *
+ * `version` is the workflow's version at the time of the read, so a different
+ * number on a later read means somebody edited the graph in between. This
+ * body is accepted verbatim by {@link replaceGraph} — read, edit one step,
+ * send it back.
+ */
+ async getGraph(id) {
+ return this.client.request({
+ method: "GET",
+ path: `/api/v1/workflows/${encodeURIComponent(id)}/graph`
+ });
+ }
+ /**
+ * Replace the whole graph in one transaction.
+ *
+ * A `PUT` and not a `PATCH`, and that is the point: a graph is nodes *plus*
+ * the edges between them, so a partial edit to a step list has no meaning
+ * without the transitions that reference it — half-applied, it would leave
+ * steps pointing at steps that no longer exist.
+ *
+ * Ids decide the outcome per step: one you send is kept and updated in place,
+ * a fresh uuid creates a step, and an id you omit deletes that step *and its
+ * run history*. Exactly one step must be a `TRIGGER`, every transition must
+ * name steps in the same document, and no step may point at itself.
+ *
+ * Refused with `409 conflict` while the workflow has running executions —
+ * those runs are standing on the steps being replaced. {@link pause} first.
+ */
+ async replaceGraph(id, body) {
+ return this.client.request({
+ method: "PUT",
+ path: `/api/v1/workflows/${encodeURIComponent(id)}/graph`,
+ body
+ });
+ }
+ /**
+ * Copy a workflow and its whole graph as a new workflow.
+ *
+ * The copy is always created disabled, whatever the original was: a clone
+ * exists to be reviewed, and one that started live would match the same
+ * trigger events as its original from the moment it appeared. Pass `{ name }`
+ * to name it; it otherwise becomes `Copy of `.
+ */
+ async clone(id, body) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/workflows/${encodeURIComponent(id)}/clone`,
+ body
+ });
+ }
+ /**
+ * Disable the workflow *and cancel every `RUNNING`/`WAITING` execution in it*,
+ * resolving `{ workflow, cancelled_executions }`.
+ *
+ * That is what separates this from `update(id, { enabled: false })`, which
+ * only stops new runs starting and leaves every in-flight contact walking the
+ * graph — the next delay still expires, the next email still sends.
+ *
+ * The cancellation is terminal: {@link resume} re-opens the workflow to new
+ * runs, it does not put the cancelled contacts back where they were.
+ */
+ async pause(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/workflows/${encodeURIComponent(id)}/pause`
+ });
+ }
+ /**
+ * Re-enable the workflow so its trigger matches again. `cancelled_executions`
+ * is always 0 here — resuming starts nothing and stops nothing.
+ *
+ * Refused with `422 validation_error` while any step is still unconfigured,
+ * the same rule `update(id, { enabled: true })` enforces: an enabled workflow
+ * accepts contacts immediately and would otherwise fail only once one reached
+ * the broken step.
+ */
+ async resume(id) {
+ return this.client.request({
+ method: "POST",
+ path: `/api/v1/workflows/${encodeURIComponent(id)}/resume`
+ });
+ }
};
// src/errors.ts
@@ -1164,7 +2198,7 @@ function errorFromResponse(statusCode, errorCode, message, body, contentType) {
}
// src/client.ts
-var SDK_VERSION = "1.0.0";
+var SDK_VERSION = "1.1.0";
var DEFAULT_BASE_URL = "https://api.sendly.now";
var Sendly = class {
emails;
@@ -1176,6 +2210,8 @@ var Sendly = class {
events;
verify;
lists;
+ /** Reusable body fragments a template includes with `{{> name}}`. */
+ snippets;
/** Receiving mailboxes. Reads only — the writes need a user, not an API key. */
mailboxes;
/** Campaigns on the versioned `/api/v1` surface. */
@@ -1190,6 +2226,12 @@ var Sendly = class {
usage;
/** The project this key belongs to, on the versioned `/api/v1` surface. */
projects;
+ /** Consent topics and what each contact has said they want. */
+ topics;
+ /** Address validation — one batch, or a whole list. */
+ validation;
+ /** Why mail from your domains is or is not arriving. */
+ deliverability;
apiKey;
baseUrl;
fetchImpl;
@@ -1220,6 +2262,7 @@ var Sendly = class {
this.events = new EventsResource(this);
this.verify = new VerifyResource(this);
this.lists = new ListsResource(this);
+ this.snippets = new SnippetsResource(this);
this.mailboxes = new MailboxesResource(this);
this.campaigns = new CampaignsResource(this);
this.segments = new SegmentsResource(this);
@@ -1227,6 +2270,9 @@ var Sendly = class {
this.analytics = new AnalyticsResource(this);
this.usage = new UsageResource(this);
this.projects = new ProjectsResource(this);
+ this.topics = new TopicsResource(this);
+ this.validation = new ValidationResource(this);
+ this.deliverability = new DeliverabilityResource(this);
}
/**
* Low-level request helper. Resources call this; consumers can call it
@@ -1386,6 +2432,7 @@ function constructEvent(payload, signature, timestamp, secret, options = {}) {
ContactsResource,
DEFAULT_BASE_URL,
DEFAULT_TOLERANCE_MS,
+ DeliverabilityResource,
DomainsResource,
EmailsResource,
EventsResource,
@@ -1404,9 +2451,12 @@ function constructEvent(payload, signature, timestamp, secret, options = {}) {
SendlyRateLimitError,
SendlyServerError,
SendlyValidationError,
+ SnippetsResource,
SuppressionResource,
TemplatesResource,
+ TopicsResource,
UsageResource,
+ ValidationResource,
VerifyResource,
WebhooksResource,
WorkflowsResource,
diff --git a/dist/index.d.cts b/dist/index.d.cts
index 4c7ad0b..688d9a7 100644
--- a/dist/index.d.cts
+++ b/dist/index.d.cts
@@ -164,7 +164,17 @@ interface paths {
delete: operations["deleteDomain"];
options?: never;
head?: never;
- patch?: never;
+ /**
+ * Assign a sending identity to a stream
+ * @description Assign this identity to transactional or marketing traffic, make it the project's default for that stream, or give it the from-address a send on that stream uses when it names none.
+ *
+ * Streams are enforced, not labelled: once an identity is assigned, a send of the other kind from it is refused with 403. That is what keeps a campaign's complaint rate off the identity your password resets go out on. An identity with no stream (`stream: null`, and the state of every domain added before this existed) carries both.
+ *
+ * At most one identity per (project, stream) is the default; setting `streamDefault` demotes whichever held it. `defaultFromAddress` has to be an address on this identity's own host — a default pointing anywhere else would go out unsigned by the name in the From header. Every field is optional and an omitted one is left alone.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
+ */
+ patch: operations["assignDomainStream"];
trace?: never;
};
"/api/domains/{id}/dodomain-session": {
@@ -282,7 +292,9 @@ interface paths {
};
/**
* Get a single email
- * @description Fetch one email along with its delivery events.
+ * @description Fetch one email together with its DELIVERY history — the transitions behind `status`, oldest first.
+ *
+ * `events` here is not the custom-event resource: the events a caller records with `POST /api/v1/events` are read from `GET /api/v1/events`, and never appear on this response.
*
* Requires the `emails:read` scope — View the emails you have sent and their delivery status.
*/
@@ -326,7 +338,7 @@ interface paths {
put?: never;
/**
* Subscribe a contact to a list
- * @description Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm?token=` to the contact itself.
+ * @description Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm-subscription?token=` to the contact itself.
*
* Accepts SENDING_ONLY (`pk_*`) keys so it can back a public subscribe form.
*
@@ -491,6 +503,73 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/mailboxes/{id}/drafts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Draft a message with AI
+ * @description Ask Sendly's assistant to write a message for a mailbox — a new email from a short brief, a rewrite of something you already have, or a set of subject lines.
+ *
+ * **It returns text and sends nothing.** The response always reports `sent: false`, and there is no argument that changes that. Putting a draft in someone's inbox is a separate operation (`sendMailboxMessage`) under a separate scope, so a client that may draft is not thereby a client that may mail your customers.
+ *
+ * That is also why this operation asks only for `mailboxes:read`: it names a mailbox so the draft can be written in that address's voice, reads no correspondence, and stores nothing.
+ *
+ * Everything you pass — the brief, the draft, the recipient context — is treated strictly as data describing what to write, never as instructions to the model.
+ *
+ * Drafting is capped at 120 requests per hour per project.
+ *
+ * Requires the `mailboxes:read` scope — View the mailboxes on your domains and their settings.
+ */
+ post: operations["draftMailboxMessage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/mailboxes/{id}/messages": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send a message from a mailbox
+ * @description Compose and send a NEW message from a hosted mailbox — a first email to someone, not a reply inside a thread that already exists. It goes out from the mailbox's own address, over its own domain, and the recipient can reply to it.
+ *
+ * **The sender is the mailbox in the path.** There is no `from` field: a route that sends under a customer's own identity must not take the identity as an argument.
+ *
+ * **The body is plain text.** Sendly renders the HTML part from it, escaping as it goes, so there is one place where text becomes markup and it is inside Sendly. HTML is not accepted.
+ *
+ * Bcc recipients are delivered to but appear in no header, which also means the copy filed in the mailbox's Sent folder does not record them.
+ *
+ * Refusals worth handling by name:
+ *
+ * - `422 RECIPIENT_SUPPRESSED` — one or more recipients are on this project's suppression list. The message names them; remove them or take them off the list.
+ * - `422 CONTENT_REFUSED` — the outbound content scanner refused the message.
+ * - `503 CONTENT_SCAN_UNAVAILABLE` — screening could not reach a verdict for a young project. Nothing was sent; retry shortly.
+ * - `429` — a mailbox may send 60 messages an hour through this endpoint.
+ *
+ * The message is stored as a new conversation on the mailbox, so the reply threads onto it.
+ *
+ * Requires the `mailboxes:send` scope — Write and send new email from your hosted mailboxes, as that address.
+ */
+ post: operations["sendMailboxMessage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/projects/{id}/api-keys": {
parameters: {
query?: never;
@@ -510,7 +589,7 @@ interface paths {
* Create an API key
* @description Mint a new API key on the project. The token is NOT returned — the response carries the key's metadata plus a one-time `revealUrl` that only a signed-in dashboard session can open.
*
- * **Scope attenuation:** a key created with a delegated credential (an OAuth token or another API key) may not carry a scope that credential does not itself hold. A request that asks for more is refused with `400 SCOPE_ESCALATION` rather than quietly narrowed, so the mistake is reported where it was made instead of surfacing later as an unexplained 403. Naming only `permission` counts as asking for every scope that permission implies.
+ * **Scope attenuation:** a key created with a delegated credential (an OAuth token or another API key) may not carry a scope that credential does not itself hold. A request that asks for more is refused with `400 SCOPE_ESCALATION` rather than quietly narrowed, so the mistake is reported where it was made instead of surfacing later as an unexplained 403. Naming only `legacyGrantPreset` counts as asking for every scope that preset implies.
*
* Requires the `api-keys:write` scope — Create, rotate, and revoke API keys — these keep working even after you disconnect this app.
*/
@@ -565,6 +644,64 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/snippets": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List snippets
+ * @description Cursor-paginated list of the project's reusable template fragments. `search` matches name and description.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["listSnippets"];
+ put?: never;
+ /**
+ * Create a snippet
+ * @description `name` is the literal identifier templates include with `{{> name}}`: it must start with a letter and contain only letters, digits, hyphen or underscore, and it is unique within the project.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ post: operations["createSnippet"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/snippets/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get a snippet
+ * @description Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["getSnippet"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a snippet
+ * @description Templates that still include the snippet keep rendering — an absent snippet renders as an empty string, exactly like an absent variable.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ delete: operations["deleteSnippet"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a snippet
+ * @description Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ patch: operations["updateSnippet"];
+ trace?: never;
+ };
"/api/suppression": {
parameters: {
query?: never;
@@ -895,6 +1032,32 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/campaigns/{id}/failures": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List a campaign's failed sends
+ * @description The recipients this campaign did not reach, read from its per-contact send ledger. The campaign counters say how many were sent; only this says WHO was dropped and why, which is what makes retrying a decision rather than a guess.
+ *
+ * `reason` comes from a fixed vocabulary, not from the underlying error text, so it is stable enough to branch on. It is `null` for rows recorded before reasons were captured.
+ *
+ * Cursor-paginated like every other v1 list. Unlike them it also returns `total`: the retry action operates on that number, and a page that can only say `has_more` cannot tell you whether 3 or 30,000 sends failed.
+ *
+ * Requires the `campaigns:read` scope — View your campaigns and their performance.
+ */
+ get: operations["v1ListCampaignFailures"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/campaigns/{id}/pause": {
parameters: {
query?: never;
@@ -939,6 +1102,32 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/campaigns/{id}/retry-failed": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Retry a campaign's failed sends
+ * @description Re-drive only the recipients whose send failed, through the same pipeline the campaign used. Nobody who was already mailed is mailed again: each ledger row is claimed before it is touched, and a row whose email was created before the failure was recorded is re-queued rather than re-sent.
+ *
+ * The retry runs in the background, so this returns as soon as it is queued, with the number of rows it was queued for. Takes no request body.
+ *
+ * Only a `SENT` campaign can be retried: a `SENDING` or `PAUSED` one still has its own send in progress against the same ledger, and a `CANCELLED` one was stopped deliberately.
+ *
+ * Requires the `campaigns:write` scope — Create, edit, and organize your campaigns.
+ */
+ post: operations["v1RetryCampaignFailures"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/campaigns/{id}/send": {
parameters: {
query?: never;
@@ -989,65 +1178,75 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/emails": {
+ "/api/v1/contacts": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- put?: never;
/**
- * Send a transactional email
- * @description Send one transactional email and receive its delivery status in the same response. Accepts a `template` id or an inline `subject` + `body`.
- *
- * This is the send to reach for when you need to know what happened. The legacy `POST /api/emails` answers with row ids and no status, so telling an accepted send from a refused one costs a second request; here the receipt carries `status`, and `from` reports the sender actually used — which is worth reading, since a template may have supplied it.
- *
- * Exactly ONE recipient. A single receipt cannot describe a fan-out, so `to` takes one address: use `cc`/`bcc` to copy others on the same message, and `POST /api/emails/batch` to send different ones.
+ * List contacts
+ * @description Cursor-paginated list of contacts, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
*
- * `202 Accepted` is the success answer, and `PENDING` the usual `status`: the message is queued for the sending pipeline, not yet handed to the provider. Later states (`DELIVERED`, `BOUNCED`, …) arrive by webhook.
+ * A cursor is bound to the filters that minted it. Changing `search` or `subscribed` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
*
- * An optional `Idempotency-Key` header (1–255 chars, 24h TTL) makes a retry safe: the first request wins and a retry carrying the same key AND body replays its receipt. Reusing a key with a different body answers 422 rather than serving another request's result.
+ * Requires the `contacts:read` scope — View your contacts and their custom fields.
+ */
+ get: operations["v1ListContacts"];
+ put?: never;
+ /**
+ * Create a contact
+ * @description Create a contact. The address is unique per project, so creating one that already exists answers `409 conflict` rather than updating the existing row — there is no upsert on this surface, because a silent update is not what a caller who wrote `create` asked for.
*
- * Requires the `emails:send` scope — Send emails from your verified domains.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
*/
- post: operations["v1SendEmail"];
+ post: operations["v1CreateContact"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/emails/test": {
+ "/api/v1/contacts/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * Retrieve a contact
+ * @description Fetch one contact by id.
+ *
+ * Requires the `contacts:read` scope — View your contacts and their custom fields.
+ */
+ get: operations["v1GetContact"];
put?: never;
+ post?: never;
/**
- * Send a sandbox test email
- * @description Prove that sending works — before any domain, DNS record or verification exists.
+ * Delete a contact
+ * @description Delete the contact row. The emails already sent to that address are NOT erased — a send is a record of something that happened, and removing the recipient does not undo it. Erasing delivery history is a separate, irreversible operation that this surface deliberately does not expose.
*
- * The message is sent FROM this project's sandbox address (`sandbox_address` on `GET /api/v1/projects`) and can only reach ONE recipient: the project owner's own verified account email, which is also what `to` defaults to. Naming any other recipient answers 403, and naming a `from` answers 422 — the sender is resolved server-side and a request that expects a different one is refused rather than quietly re-addressed.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
+ */
+ delete: operations["v1DeleteContact"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a contact
+ * @description Partial update. Omitted fields are left alone.
*
- * That restriction is the reason `emails:test` is a separate, non-sensitive scope: a call under it cannot put mail in a stranger's inbox, so it can sit in a default grant where `emails:send` may not. It is not a way to send real mail cheaply — use `POST /api/v1/emails` for that.
+ * `email` is NOT writable: an address is the contact's identity on this API, and rewriting it in place would silently change what every earlier send was addressed to. Create the new address instead.
*
- * Sandbox sends are capped per project per day, and the response's `sandbox: true` marks the receipt so a relayed summary cannot pass a test send off as a real one.
+ * `custom_fields` REPLACES the stored object rather than merging into it, so a key you omit is gone. Read the contact first if you mean to change one key and keep the rest.
*
- * Requires the `emails:test` scope — Send test emails to your own address from the Sendly sandbox.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
*/
- post: operations["v1SendTestEmail"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
+ patch: operations["v1UpdateContact"];
trace?: never;
};
- "/api/v1/events": {
+ "/api/v1/contacts/{id}/topics": {
parameters: {
query?: never;
header?: never;
@@ -1055,37 +1254,25 @@ interface paths {
cookie?: never;
};
/**
- * List events
- * @description Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.
- *
- * A cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.
- *
- * Requires the `events:read` scope — View the custom events your application has recorded.
- */
- get: operations["v1ListEvents"];
- put?: never;
- /**
- * Record an event
- * @description Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.
- *
- * `contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.
+ * Get a contact's topic preferences
+ * @description Everything this contact has said they want, as the send path reads it.
*
- * Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.
- *
- * This endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.
+ * `subscribed` on each topic is the EFFECTIVE answer: a contact who has never answered has no row at all, and the topic's `default_opt_in` decides what that silence means. It is folded in here so no caller has to reimplement the rule.
*
- * Sending-only (`pk_*`) keys cannot record events — they hold the send capability and nothing else.
+ * The top-level `subscribed` is the global marketing opt-out, and it OUTRANKS every topic. A caller reading only the topic list would conclude somebody is reachable when they are not.
*
- * Requires the `events:write` scope — Record custom events for your contacts.
+ * Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
*/
- post: operations["v1TrackEvent"];
+ get: operations["v1GetContactTopicPreferences"];
+ put?: never;
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/events/names": {
+ "/api/v1/deliverability/diagnose": {
parameters: {
query?: never;
header?: never;
@@ -1093,12 +1280,16 @@ interface paths {
cookie?: never;
};
/**
- * List event names
- * @description Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.
+ * Diagnose why mail from a domain is not arriving
+ * @description Reads the sending domain's DNS identity, the project's delivery outcomes over the last `window_days`, and — when an `address` is given — that recipient's suppression state, then publishes `findings`: what is actually wrong, worst first, each with a stable `code` and the fix. Branch on `code`, never on the prose.
*
- * Requires the `events:read` scope — View the custom events your application has recorded.
+ * Everything here is read from data the platform already holds. The DNS statuses are the cached results of the verification refresh job, NOT a live lookup — `identity.last_checked_at` says when they were filled, and a domain that has never been checked reports nulls with a `dns_never_checked` finding rather than failures.
+ *
+ * `recent_delivery` is PROJECT-WIDE, not per-domain, because an email row records no sending domain; the field says so in its own `scope`. Rates are suppressed as meaningless below 20 sends in the window.
+ *
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1ListEventNames"];
+ get: operations["v1DiagnoseDeliverability"];
put?: never;
post?: never;
delete?: never;
@@ -1107,7 +1298,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/events/stats": {
+ "/api/v1/deliverability/dmarc": {
parameters: {
query?: never;
header?: never;
@@ -1115,14 +1306,18 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve event counts
- * @description Per-name event counts over a bounded window, most frequent first.
+ * DMARC aggregate reports for your domains
+ * @description DMARC aggregate (RUA) reports receiving providers have sent about your verified domains, newest reporting window first.
*
- * The window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.
+ * The only signal on this surface that does not come from us. A report is a receiver saying what it saw arrive claiming to be your domain, from every source — which is how a sender finds out both that their own alignment is broken and that somebody else is sending as them.
*
- * Requires the `events:read` scope — View the custom events your application has recorded.
+ * `pass_count` counts DMARC ALIGNMENT from `policy_evaluated`, not raw authentication results: a message can pass SPF for a domain that is not the one in its From header, and that is precisely the case DMARC exists to catch.
+ *
+ * Only reports about a domain registered in this project are stored, so a report about a domain you have not added will not appear here.
+ *
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1GetEventStats"];
+ get: operations["v1ListDmarcReports"];
put?: never;
post?: never;
delete?: never;
@@ -1131,7 +1326,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/projects": {
+ "/api/v1/deliverability/domains": {
parameters: {
query?: never;
header?: never;
@@ -1139,16 +1334,16 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve the authenticated project
- * @description The project the presented credential is scoped to — resolved from the API key, or from the `x-project-id` header for a session or delegated token. Singular despite the plural path, like `/api/v1/usage`: every v1 operation acts on exactly one project.
+ * Delivery outcomes per recipient domain
+ * @description Sent, delivered, bounced, complained and opened counts split by the RECIPIENT's domain and by UTC day, newest day first.
*
- * `sandbox_address` is this project's quick-start sender. It works with no domain setup, but only to the project owner's own verified address and under a daily cap — it is how you prove sending works end to end before any DNS exists. It is null when no handle can be derived (a project with no owner).
+ * This is the axis `diagnose` cannot report: its `recent_delivery` is project-wide, because an email row records no sending domain. A project-wide bounce rate hides the case that matters most — one recipient domain refusing almost everything while the rest is healthy.
*
- * To enumerate every project you belong to — a different question, and not project-scoped — use `GET /api/users/me/projects`.
+ * The counts are maintained by an hourly job over a rolling 30-day window, NOT computed on request; `computed_at` on each row says when it was last rebuilt. No rate is published: a rate over three sends is not information, and the counts let you apply your own threshold.
*
- * Requires the `projects:read` scope — View your projects and their settings.
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1GetProject"];
+ get: operations["v1ListRecipientDomainStats"];
put?: never;
post?: never;
delete?: never;
@@ -1157,7 +1352,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/segments": {
+ "/api/v1/domains": {
parameters: {
query?: never;
header?: never;
@@ -1165,31 +1360,35 @@ interface paths {
cookie?: never;
};
/**
- * List segments
- * @description Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ * List sending domains
+ * @description Cursor-paginated list of sending domains, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
*
- * `member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.
+ * `verified` is SES's verdict on the identity and is the field that decides whether mail can leave from this domain. `dkim_verified` is a separate fact — what the DNS health refresh last read for the DKIM records — and the two disagree while a re-check is in flight, so do not treat either as a spelling of the other.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * Requires the `domains:read` scope — View your sending domains and their verification status.
*/
- get: operations["v1ListSegments"];
+ get: operations["v1ListDomains"];
put?: never;
/**
- * Create a segment
- * @description Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.
+ * Add a sending domain
+ * @description Register a domain and start SES DKIM verification. The response carries the identity as created — `verified` is false, because nothing is verified until the DKIM records are published in the domain's DNS and SES resolves them. Poll `/api/v1/domains/{id}/verify` after publishing them.
*
- * A `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.
+ * `region` pins the SES region. The first domain a project adds LOCKS the project to that region and every later domain must match it — a project split across regions would have its SES configuration diverge silently.
*
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ * `stream` assigns the identity to transactional or marketing traffic at creation; omit it to leave the identity serving both. `stream_default` makes it the project's default for that stream and therefore requires `stream` — sending it alone answers `422 validation_error` rather than being ignored.
+ *
+ * A host already registered — to this project or to another — answers `409 conflict` when the caller can already send from it and `403 project_access_denied` when it belongs elsewhere. A subdomain whose registrable root is held by a suspended project is refused the same way.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- post: operations["v1CreateSegment"];
+ post: operations["v1CreateDomain"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/segments/{id}": {
+ "/api/v1/domains/{id}": {
parameters: {
query?: never;
header?: never;
@@ -1197,118 +1396,113 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve a segment
- * @description Fetch one segment, including its saved `condition` and materialized `member_count`.
+ * Retrieve a sending domain
+ * @description Fetch one sending domain by id.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * Requires the `domains:read` scope — View your sending domains and their verification status.
*/
- get: operations["v1GetSegment"];
+ get: operations["v1GetDomain"];
put?: never;
post?: never;
/**
- * Delete a segment
- * @description Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.
+ * Remove a sending domain
+ * @description Remove the domain from the project. Refused with `409 conflict` while a template, workflow step or active campaign still sends from an address on this host — repoint those first, or their sends would start failing at SES with nothing here explaining why.
*
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ * The underlying SES identity is dropped too, unless another project still holds the same host. Its DKIM keys go with it, so re-adding the domain later mints new records that have to be published again.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- delete: operations["v1DeleteSegment"];
+ delete: operations["v1DeleteDomain"];
options?: never;
head?: never;
- /**
- * Update a segment
- * @description Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.
- *
- * `type` is not accepted here — see the create operation.
- *
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
- */
- patch: operations["v1UpdateSegment"];
+ patch?: never;
trace?: never;
};
- "/api/v1/segments/{id}/contacts": {
+ "/api/v1/domains/{id}/verify": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * List the contacts in a segment
- * @description Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.
+ * Refresh a sending domain's verification state
+ * @description Re-read this domain's state from SES and DNS and return the refreshed document.
*
- * Cursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.
+ * This does NOT perform verification. Verification happens in the domain's own DNS, when its owner publishes the DKIM records SES minted at creation; Amazon decides when they resolve. What this call does is ask SES what it currently sees, re-check SPF and DMARC, and persist the answer — so a caller polling after a DNS change learns the outcome without waiting for the periodic sweep. Calling it on a domain whose records are not published yet is not an error and does not make it verify any sooner.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * A POST rather than a GET because it writes: the refreshed state is persisted, and a verified/unverified transition notifies the project.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- get: operations["v1ListSegmentContacts"];
- put?: never;
- post?: never;
+ post: operations["v1VerifyDomain"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/usage": {
+ "/api/v1/email-validations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * Retrieve current usage and limits
- * @description Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.
+ * Validate a batch of email addresses
+ * @description Check up to 50 addresses for whether they can receive mail, and for the signals that make one worth mailing. Billed per address.
*
- * Every figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.
+ * The response publishes a `verdict` alongside the flags it was drawn from. Branch on the verdict: the two obvious readings of the flags are both wrong — a free-provider address (`is_personal`) and a role mailbox (`is_role_address`) are ordinary, deliverable addresses that real customers use, and refusing them would shrink a list for no reason. `is_disposable` is the only flag that lowers a verdict.
*
- * Two caveats worth reading before you alert on these numbers:
+ * `unknown` means DNS did not answer in time, so that address was NOT checked. It is a separate value from `undeliverable` on purpose — acting on the two together deletes live contacts over a network hiccup.
*
- * - The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.
- * - `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.
+ * The 50-address ceiling is a latency bound, not a payload one: every distinct DOMAIN in the batch costs a DNS round trip. To validate a whole list, use `POST /api/v1/lists/{id}/validation-runs`, which runs as a background job.
*
- * Requires the `usage:read` scope — View your usage totals and billing limits.
+ * Requires the `validation:write` scope — Check whether email addresses can receive mail — this is billed per address.
*/
- get: operations["v1GetUsage"];
- put?: never;
- post?: never;
+ post: operations["v1ValidateEmails"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows": {
+ "/api/v1/emails": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * List workflows
- * @description Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ * Send a transactional email
+ * @description Send one transactional email and receive its delivery status in the same response. Accepts a `template` id or an inline `subject` + `body`.
*
- * Unlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.
+ * This is the send to reach for when you need to know what happened. The legacy `POST /api/emails` answers with row ids and no status, so telling an accepted send from a refused one costs a second request; here the receipt carries `status`, and `from` reports the sender actually used — which is worth reading, since a template may have supplied it.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
- */
- get: operations["v1ListWorkflows"];
- put?: never;
- /**
- * Create a workflow
- * @description Creates an event-triggered workflow with a single trigger step. The rest of the graph (emails, delays, conditions) is built in the dashboard, so a workflow is created disabled and stays inert until it has steps to run.
+ * Exactly ONE recipient. A single receipt cannot describe a fan-out, so `to` takes one address: use `cc`/`bcc` to copy others on the same message, and `POST /api/emails/batch` to send different ones.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * `202 Accepted` is the success answer, and `PENDING` the usual `status`: the message is queued for the sending pipeline, not yet handed to the provider. Later states (`DELIVERED`, `BOUNCED`, …) arrive by webhook.
+ *
+ * An optional `Idempotency-Key` header (1–255 chars, 24h TTL) makes a retry safe: the first request wins and a retry carrying the same key AND body replays its receipt. Reusing a key with a different body answers 422 rather than serving another request's result.
+ *
+ * Requires the `emails:send` scope — Send emails from your verified domains.
*/
- post: operations["v1CreateWorkflow"];
+ post: operations["v1SendEmail"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/executions/{execution_id}/cancel": {
+ "/api/v1/emails/test": {
parameters: {
query?: never;
header?: never;
@@ -1318,19 +1512,25 @@ interface paths {
get?: never;
put?: never;
/**
- * Cancel a workflow execution
- * @description Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.
+ * Send a sandbox test email
+ * @description Prove that sending works — before any domain, DNS record or verification exists.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * The message is sent FROM this project's sandbox address (`sandbox_address` on `GET /api/v1/projects`) and can only reach ONE recipient: the project owner's own verified account email, which is also what `to` defaults to. Naming any other recipient answers 403, and naming a `from` answers 422 — the sender is resolved server-side and a request that expects a different one is refused rather than quietly re-addressed.
+ *
+ * That restriction is the reason `emails:test` is a separate, non-sensitive scope: a call under it cannot put mail in a stranger's inbox, so it can sit in a default grant where `emails:send` may not. It is not a way to send real mail cheaply — use `POST /api/v1/emails` for that.
+ *
+ * Sandbox sends are capped per project per day, and the response's `sandbox: true` marks the receipt so a relayed summary cannot pass a test send off as a real one.
+ *
+ * Requires the `emails:test` scope — Send test emails to your own address from the Sendly sandbox.
*/
- post: operations["v1CancelWorkflowExecution"];
+ post: operations["v1SendTestEmail"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}": {
+ "/api/v1/events": {
parameters: {
query?: never;
header?: never;
@@ -1338,35 +1538,37 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve a workflow
- * @description The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.
+ * List events
+ * @description Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * A cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.
+ *
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1GetWorkflow"];
+ get: operations["v1ListEvents"];
put?: never;
- post?: never;
/**
- * Delete a workflow
- * @description Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.
+ * Record an event
+ * @description Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
- */
- delete: operations["v1DeleteWorkflow"];
- options?: never;
- head?: never;
- /**
- * Update a workflow
- * @description Sparse update — omitted fields are left unchanged.
+ * `contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.
*
- * Two state rules apply: the trigger (`event_name`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ * Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * This endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.
+ *
+ * Sending-only (`pk_*`) keys cannot record events — they hold the send capability and nothing else.
+ *
+ * Requires the `events:write` scope — Record custom events for your contacts.
*/
- patch: operations["v1UpdateWorkflow"];
+ post: operations["v1TrackEvent"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}/executions": {
+ "/api/v1/events/names": {
parameters: {
query?: never;
header?: never;
@@ -1374,29 +1576,21 @@ interface paths {
cookie?: never;
};
/**
- * List a workflow's executions
- * @description One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.
+ * List event names
+ * @description Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1ListWorkflowExecutions"];
+ get: operations["v1ListEventNames"];
put?: never;
- /**
- * Start a workflow for a contact
- * @description Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.
- *
- * 409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.
- *
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
- */
- post: operations["v1StartWorkflowExecution"];
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}/stats": {
+ "/api/v1/events/stats": {
parameters: {
query?: never;
header?: never;
@@ -1404,12 +1598,14 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve workflow statistics
- * @description Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.
+ * Retrieve event counts
+ * @description Per-name event counts over a bounded window, most frequent first.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * The window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.
+ *
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1GetWorkflowStats"];
+ get: operations["v1GetEventStats"];
put?: never;
post?: never;
delete?: never;
@@ -1418,27 +1614,41 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/verify": {
+ "/api/v1/lists": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * List subscriber lists
+ * @description Cursor-paginated list of subscriber lists, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `member_count` counts memberships in EVERY status — `PENDING` and `UNSUBSCRIBED` included — so it is the size of the membership table for this list, not the number of people currently subscribed. Read `GET /api/lists/{id}/members?status=CONFIRMED` when you want the latter.
+ *
+ * Requires the `lists:read` scope — View your subscriber lists and who is on them.
+ */
+ get: operations["v1ListLists"];
put?: never;
/**
- * Validate an email address
- * @description Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.
+ * Create a subscriber list
+ * @description Create an empty subscriber list. `member_count` on the response is 0 because the list has just been created — add contacts with `POST /api/lists/{id}/subscribe`.
+ *
+ * **`double_opt_in` does not make Sendly send anything.** With it on, `POST /api/lists/{id}/subscribe` creates the membership as `PENDING` and returns a `confirm_token`; delivering `/api/lists/confirm-subscription?token=` to the contact is YOUR job. A list that turns the flag on without sending that link collects pending memberships and confirms none of them.
+ *
+ * `description`, `confirmation_template_id` and `redirect_url` accept `null`, which means the same as omitting them: the field is left unset.
+ *
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
*/
- post: operations["verifyEmailAddress"];
+ post: operations["v1CreateList"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/webhooks": {
+ "/api/v1/lists/{id}": {
parameters: {
query?: never;
header?: never;
@@ -1446,57 +1656,63 @@ interface paths {
cookie?: never;
};
/**
- * List user webhooks
- * @description List all user-managed outbound webhooks for the auth'd project (secrets are not returned).
+ * Retrieve a subscriber list
+ * @description Fetch one list by id, with the same status-agnostic `member_count` the collection returns.
*
- * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ * Requires the `lists:read` scope — View your subscriber lists and who is on them.
*/
- get: operations["listWebhooks"];
+ get: operations["v1GetList"];
put?: never;
+ post?: never;
/**
- * Create a webhook
- * @description Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.
+ * Delete a subscriber list
+ * @description Delete the list and, by cascade, every membership on it. Those memberships are the consent record: an `UNSUBSCRIBED` row is the evidence that someone opted out, and it goes with the list — re-creating the list and re-importing the same addresses will not find their opt-outs waiting. The emails already sent are untouched.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
*/
- post: operations["createWebhook"];
- delete?: never;
+ delete: operations["v1DeleteList"];
options?: never;
head?: never;
- patch?: never;
+ /**
+ * Update a subscriber list
+ * @description Partial update. Omitted fields are left alone, and `null` for `description`, `confirmation_template_id` or `redirect_url` is treated the same as omitting them — this surface cannot clear a field back to empty yet.
+ *
+ * **`double_opt_in` does not make Sendly send anything.** With it on, `POST /api/lists/{id}/subscribe` creates the membership as `PENDING` and returns a `confirm_token`; delivering `/api/lists/confirm-subscription?token=` to the contact is YOUR job. A list that turns the flag on without sending that link collects pending memberships and confirms none of them.
+ *
+ * Turning `double_opt_in` on affects only memberships created afterwards. Existing `CONFIRMED` memberships stay confirmed: those contacts consented under the rule in force when they subscribed, and demoting them would revoke a consent record rather than collect one.
+ *
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
+ */
+ patch: operations["v1UpdateList"];
trace?: never;
};
- "/api/webhooks/{id}": {
+ "/api/v1/lists/{id}/validation-runs": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /**
- * Get a webhook
- * @description Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
- */
- get: operations["getWebhook"];
+ get?: never;
put?: never;
- post?: never;
/**
- * Delete a webhook
- * @description Hard-delete a webhook. Cascades to all WebhookCall rows.
+ * Validate every address on a list
+ * @description Start a background run over every contact on the list and answer immediately with the run. Poll `GET /api/v1/validation-runs/{id}` for progress and read the verdicts from its `/results` page.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * This VALIDATES and changes nothing: no membership is unsubscribed, no contact is deleted. What to do about an `undeliverable` address is your decision, and a run that acted on its own findings would be acting on a DNS lookup that can also answer `unknown`.
+ *
+ * A second run while one is already in flight for the same list is refused with 409 rather than queued: two runs would bill the same addresses twice.
+ *
+ * Requires the `validation:write` scope — Check whether email addresses can receive mail — this is billed per address.
*/
- delete: operations["deleteWebhook"];
+ post: operations["v1StartListValidationRun"];
+ delete?: never;
options?: never;
head?: never;
- /**
- * Update a webhook
- * @description Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
- */
- patch: operations["updateWebhook"];
+ patch?: never;
trace?: never;
};
- "/api/webhooks/{id}/calls": {
+ "/api/v1/projects": {
parameters: {
query?: never;
header?: never;
@@ -1504,12 +1720,16 @@ interface paths {
cookie?: never;
};
/**
- * List recent webhook calls
- * @description Cursor-paginated list of recent delivery attempts for a single webhook.
+ * Retrieve the authenticated project
+ * @description The project the presented credential is scoped to — resolved from the API key, or from the `x-project-id` header for a session or delegated token. Singular despite the plural path, like `/api/v1/usage`: every v1 operation acts on exactly one project.
*
- * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ * `sandbox_address` is this project's quick-start sender. It works with no domain setup, but only to the project owner's own verified address and under a daily cap — it is how you prove sending works end to end before any DNS exists. It is null when no handle can be derived (a project with no owner).
+ *
+ * To enumerate every project you belong to — a different question, and not project-scoped — use `GET /api/users/me/projects`.
+ *
+ * Requires the `projects:read` scope — View your projects and their settings.
*/
- get: operations["listWebhookCalls"];
+ get: operations["v1GetProject"];
put?: never;
post?: never;
delete?: never;
@@ -1518,2667 +1738,9623 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/webhooks/{id}/rotate-secret": {
+ "/api/v1/segments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * List segments
+ * @description Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1ListSegments"];
put?: never;
/**
- * Rotate the webhook signing secret
- * @description Generate a new shared secret. Returns the new plaintext secret exactly once.
+ * Create a segment
+ * @description Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * A `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
*/
- post: operations["rotateWebhookSecret"];
+ post: operations["v1CreateSegment"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
-}
-interface components {
- schemas: {
- /** @description Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region. */
- AddDomainBody: {
- domain: string;
- /** Format: uuid */
- projectId?: string;
- /**
- * @description Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region.
- * @enum {string}
- */
- region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ "/api/v1/segments/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/suppression — manually add an email to the suppression list. */
- AddSuppression: {
- /** Format: email */
- email: string;
- /**
- * @default MANUAL
- * @enum {string}
- */
- reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /**
+ * Retrieve a segment
+ * @description Fetch one segment, including its saved `condition` and materialized `member_count`.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1GetSegment"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a segment
+ * @description Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ */
+ delete: operations["v1DeleteSegment"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a segment
+ * @description Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.
+ *
+ * `type` is not accepted here — see the create operation.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ */
+ patch: operations["v1UpdateSegment"];
+ trace?: never;
+ };
+ "/api/v1/segments/{id}/contacts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Campaign counters and engagement over the window. */
- AnalyticsCampaignStatsV1: {
- /** @description Campaigns in DRAFT or SCHEDULED. */
- active: number;
- average_click_rate: number;
- /** @description Percentage, one decimal place. */
- average_open_rate: number;
- completed: number;
- total: number;
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * List the contacts in a segment
+ * @description Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.
+ *
+ * Cursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1ListSegmentContacts"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/suppressions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Daily email counters across the window. Every day in range is present, zero-filled. */
- AnalyticsTimeseriesV1: {
- data: {
- bounces: number;
- clicks: number;
- /** Format: date-time */
- date: string;
- delivered: number;
- emails: number;
- opens: number;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * List suppressed addresses
+ * @description Cursor-paginated list of suppressed addresses, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * A cursor is bound to the filters that minted it. Changing `reason` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
+ *
+ * Requires the `suppression:read` scope — View the addresses on your suppression list.
+ */
+ get: operations["v1ListSuppressions"];
+ put?: never;
+ /**
+ * Suppress an address
+ * @description Add an address to this project's suppression list, so no further send reaches it.
+ *
+ * Idempotent: suppressing an already-suppressed address answers `201` with the EXISTING record rather than `409`. The first `reason` and `source` win, because a later manual entry must not overwrite what an SES bounce recorded.
+ *
+ * `source` is NOT accepted in the body — it is derived from the credential (`API` for an API key, `DASHBOARD` for a session), so a record's provenance cannot be dressed up as a deliverability fact.
+ *
+ * Requires the `suppression:write` scope — Add and remove addresses on your suppression list.
+ */
+ post: operations["v1CreateSuppression"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/suppressions/{email}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Sent campaigns ranked by open rate. */
- AnalyticsTopCampaignsV1: {
- data: {
- click_rate: number;
- clicked: number;
- /** Format: uuid */
- id: string;
- open_rate: number;
- opened: number;
- sent: number;
- subject: string;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * Check whether an address is suppressed
+ * @description Fetch the suppression record for one address. The path parameter is the address itself, URL-encoded.
+ *
+ * An address that is NOT suppressed answers `404 resource_not_found` — this path addresses the suppression record, and there is none. The answer is definite either way: `200` means suppressed and says why, `404` means not suppressed.
+ *
+ * A `200` may also come from a platform-wide block recorded outside this project, in which case the address is genuinely undeliverable for you even though you never suppressed it.
+ *
+ * Requires the `suppression:read` scope — View the addresses on your suppression list.
+ */
+ get: operations["v1GetSuppression"];
+ put?: never;
+ post?: never;
+ /**
+ * Remove an address from the suppression list
+ * @description Clear Sendly's suppression record for this address, so the send pipeline stops refusing it. This is the one operation on this surface that can put mail back into an inbox that asked you to stop, which is why `suppression:write` is a sensitive scope.
+ *
+ * It does NOT remove the address from AWS SES's own account-level suppression list. SES maintains that list independently of anything Sendly stores, so an address SES suppressed after a hard bounce stays undeliverable through SES even once this record is gone — removing it here is not a promise that the next send arrives.
+ *
+ * Idempotent: an address that was never suppressed answers `200` too, because "not on the list" is the state you asked for.
+ *
+ * Requires the `suppression:write` scope — Add and remove addresses on your suppression list.
+ */
+ delete: operations["v1DeleteSuppression"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/templates": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description The time range this response was computed over, after the 90-day clamp. */
- AnalyticsWindowV1: {
- /** Format: date-time */
- from: string;
- /** Format: date-time */
- to: string;
+ /**
+ * List templates
+ * @description Cursor-paginated list of templates, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `search` is a case-insensitive substring match on the NAME only — narrower than the dashboard's search, which also reads description and subject, because a caller that asked for a name match should not be handed rows that merely mention the word in their body.
+ *
+ * A cursor is bound to the filters that minted it. Changing `search` or `email_category` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["v1ListTemplates"];
+ put?: never;
+ /**
+ * Create a template
+ * @description Create a template. The `from` domain must already be a verified sending identity for this project — an unverified sender answers `403 forbidden` here rather than becoming a campaign that fails at send time.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ post: operations["v1CreateTemplate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/templates/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
- ApiKey: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /** Format: uuid */
- domainId: string | null;
- /** Format: uuid */
- id: string;
- /** @description Last 4 characters of the token — the only fragment of the secret that survives creation. */
- lastFour: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- lastUsedAt: string | null;
- name: string;
- /** @enum {string} */
- permission: "FULL" | "SENDING_ONLY";
- /** Format: uuid */
- projectId: string;
- /**
- * Format: date-time
- * @description Set once the key is revoked. Revoked keys are NOT filtered out of list/get responses.
- */
- revokedAt: string | null;
- /** @description The explicit scope grant this key carries. Empty on a key minted before this column existed — that row's grant is derived from `permission` instead at request time. */
- scopes: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test")[];
+ /**
+ * Retrieve a template
+ * @description Fetch one template by id.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["v1GetTemplate"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a template
+ * @description Delete the template. Answers `409 conflict` while a workflow step or an active campaign (DRAFT, SCHEDULED or SENDING) still points at it — removing it would leave those referring to content that no longer exists, and the failure would surface at send time instead of here. The emails already sent from this template are NOT erased.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ delete: operations["v1DeleteTemplate"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a template
+ * @description Partial update. Omitted fields are left alone.
+ *
+ * Changing `subject`, `body`, `from`, `from_name` or `reply_to` snapshots the previous content into the template's version history and increments `version`; changing only `name`, `description` or `email_category` does not, because neither is content a send would have rendered.
+ *
+ * A `from` supplied here is verified before anything is written, on the same terms as create.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ patch: operations["v1UpdateTemplate"];
+ trace?: never;
+ };
+ "/api/v1/topics": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Every API key on the project, including revoked ones — filter on `revokedAt` for live keys. */
- ApiKeyListResponse: {
- data: components["schemas"]["ApiKey"][];
- /** @enum {boolean} */
- success: true;
+ /**
+ * List topics
+ * @description The subjects this project mails about, cursor-paginated and newest first.
+ *
+ * Archived topics are omitted unless you ask for them with `include_archived`. Archiving is the retire button and there is no delete: a topic is where people's answers are recorded, so removing it would remove the choices they made.
+ *
+ * `subscribed_count` counts contacts who explicitly said yes. It reads low on a topic with `default_opt_in` true, and that is the honest number — it is how many people answered, not how many would currently receive the mail.
+ *
+ * Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
+ */
+ get: operations["v1ListTopics"];
+ put?: never;
+ /**
+ * Create a topic
+ * @description `key` is the stable, project-unique name every preference form and integration refers to, so it survives a rename of `name` and cannot be edited afterwards.
+ *
+ * `default_opt_in` is the field worth thinking about: it decides what SILENCE means for every contact who never answers. Leave it true for a topic introduced over a list you already have — those contacts consented to hear from you, and inventing an opt-out they never asked for would mute mail they expect. Set it false for anything a person has to ask for, and absence then means `not asked` rather than `no`.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ post: operations["v1CreateTopic"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/topics/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An IMAP/SMTP credential for one mailbox, described but never reproduced. */
- AppPassword: {
- /** Format: date-time */
- createdAt: string;
- /** Format: uuid */
- id: string;
- /** @description The last four characters of the secret — enough to tell two credentials apart, and nothing more. */
- lastFour: string;
- /**
- * Format: date-time
- * @description Null until a mail client has authenticated with it at least once.
- */
- lastUsedAt: string | null;
- /** @description What the credential is for, e.g. `Thunderbird on my laptop`. */
- name: string;
- /** @description Which protocols this password may authenticate. `imap` reads, `smtp` sends. */
- scopes: ("imap" | "smtp")[];
- };
- /** @description A newly created app password, handed over as a one-time link rather than as a secret. */
- AppPasswordReveal: {
- /** Format: uuid */
- id: string;
- /**
- * Format: date-time
- * @description When the link stops working. Five minutes after creation; the password itself does not expire.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A single-use link that shows the password once, in a browser. Opening it requires a signed-in Sendly session belonging to a project admin — the connection that created the password cannot open it, and the second attempt to open it fails whoever makes it.
- */
- revealUrl: string;
- };
- /** @description Per-row result in a batch send response. */
- BatchEntryResult: {
- data?: components["schemas"]["SendEmailData"];
- error?: {
- code: string;
- message: string;
- };
- index: number;
- /** @enum {string} */
- status: "ok" | "error";
+ /**
+ * Retrieve a topic
+ * @description Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
+ */
+ get: operations["v1GetTopic"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Update a topic
+ * @description Rename it, re-describe it, flip `default_opt_in`, or archive it.
+ *
+ * `key` is absent from the body on purpose. Every stored preference and every integration refers to a topic by its key, so changing one would orphan them silently. There is no DELETE for the same reason — `archived: true` removes the topic from the preference centre and from new sends while every opt-out recorded against it survives.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ patch: operations["v1UpdateTopic"];
+ trace?: never;
+ };
+ "/api/v1/topics/{id}/subscriptions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Batch send wrapper. Up to 100 entries. */
- BatchSendBody: {
- emails: components["schemas"]["SendEmail"][];
+ get?: never;
+ put?: never;
+ /**
+ * Subscribe or unsubscribe a contact from a topic
+ * @description The two directions behave differently, and the asymmetry is deliberate: consent needs proof, withdrawal of consent does not.
+ *
+ * `subscribed: true` does NOT subscribe anyone. It parks the contact at `pending` and returns a `confirmation_url`; nothing is mailed on this topic until somebody opens it. There is no parameter to skip that step. A caller asserting a subscription is not evidence that the mailbox holder agreed — anyone can type an address into a form — and treating the assertion as consent is the exact failure double opt-in exists to prevent. Sendly does not send the confirmation email; you do, from your own verified domain, because it is your relationship with the contact and your sending reputation.
+ *
+ * `subscribed: false` records the opt-out immediately. Requiring somebody to confirm that they want to stop is a dark pattern, and it is also the fastest route to a spam report.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ post: operations["v1SetTopicSubscription"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/usage": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200. */
- BatchSendResponse: {
- data: components["schemas"]["BatchEntryResult"][];
- success: boolean;
+ /**
+ * Retrieve current usage and limits
+ * @description Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.
+ *
+ * Every figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.
+ *
+ * Two caveats worth reading before you alert on these numbers:
+ *
+ * - The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.
+ * - `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.
+ *
+ * Requires the `usage:read` scope — View your usage totals and billing limits.
+ */
+ get: operations["v1GetUsage"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/validation-runs/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description A campaign as exposed on the v1 API. */
- CampaignV1: {
- /** @enum {string} */
- audience_type: "ALL" | "FILTERED" | "SEGMENT";
- /** Format: date-time */
- created_at: string;
- /** Format: uuid */
- id: string;
- name: string;
- /** Format: date-time */
- scheduled_at: string | null;
- /** Format: date-time */
- sent_at: string | null;
- stats: {
- bounced: number;
- clicked: number;
- delivered: number;
- opened: number;
- sent: number;
- total_recipients: number;
- };
- /** @enum {string} */
- status: "DRAFT" | "SCHEDULED" | "SENDING" | "PAUSED" | "SENT" | "CANCELLED";
- subject: string;
+ /**
+ * Retrieve a validation run
+ * @description How far a run has got and what it found. The counters are advanced by each batch in the same step that commits the rows they count, so they are a ledger rather than a cached estimate and may be branched on.
+ *
+ * There is no total to divide by, deliberately: a list changes size while a run walks it, so a denominator captured when the run started would be wrong by the time you read it. A run is finished when `status` is `completed` or `failed`, never when a percentage reaches 100.
+ *
+ * Requires the `validation:read` scope — View your email validation runs and their results.
+ */
+ get: operations["v1GetValidationRun"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/validation-runs/{id}/results": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, and `audience_condition` is required when it is `FILTERED`. */
- CampaignV1Create: {
- audience_condition?: components["schemas"]["FilterConditionV1"];
- /**
- * @description `ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`.
- * @enum {string}
- */
- audience_type: "ALL" | "FILTERED" | "SEGMENT";
- body: string;
- description?: string;
- /**
- * Format: email
- * @description Sender address. Its domain must be verified for this project.
- */
- from: string;
- from_name?: string | null;
- name: string;
- /** Format: email */
- reply_to?: string | null;
- /** Format: uuid */
- segment_id?: string;
- subject: string;
- /**
- * @default MARKETING
- * @enum {string}
- */
- type: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ /**
+ * List a validation run's results
+ * @description One page of a run's verdicts, cursor-paginated. Filter with `verdict` — `undeliverable` is the page you want when you are about to act on the results.
+ *
+ * No total is reported: a run over a large list holds millions of rows, and counting them on every page read is the query this design exists to avoid. The run's own counters carry the numbers worth having.
+ *
+ * `contact_id` is the contact the address belonged to when it was checked, and it is null for a contact deleted since. The `email` is stored on the result rather than read through the contact, so a completed run reports what it actually checked even after the contact changed address.
+ *
+ * Requires the `validation:read` scope — View your email validation runs and their results.
+ */
+ get: operations["v1ListValidationRunResults"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Acknowledgement that a campaign was deleted. */
- CampaignV1Deleted: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ /**
+ * List webhooks
+ * @description Cursor-paginated list of webhook endpoints, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * Signing secrets are not on this response and cannot be read back — see `POST /api/v1/webhooks/{id}/rotate-secret` if you have lost one.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["v1ListWebhooks"];
+ put?: never;
+ /**
+ * Create a webhook
+ * @description Register an endpoint to receive HMAC-signed deliveries for the events named in `event_types`.
+ *
+ * The response carries the signing secret ONCE. It is shown here and by `POST /api/v1/webhooks/{id}/rotate-secret`, and by nothing else — no endpoint reads it back, so store it now. A secret you have lost is replaced by rotating, not recovered.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["v1CreateWebhook"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Cursor-paginated list of campaigns. */
- CampaignV1List: {
- data: components["schemas"]["CampaignV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ /**
+ * Retrieve a webhook
+ * @description Fetch one webhook endpoint by id. The signing secret is not part of this response.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["v1GetWebhook"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a webhook
+ * @description Remove the endpoint and its delivery history — a delivery attempt is a fact about this endpoint and has no meaning once the endpoint is gone. Deliveries already in flight are not recalled, so the endpoint may still receive an event shortly after this returns.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ delete: operations["v1DeleteWebhook"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a webhook
+ * @description Partial update. Omitted fields are left alone.
+ *
+ * `event_types` REPLACES the stored subscription list rather than merging into it, so an event you omit is unsubscribed. Setting `status` back to `ACTIVE` from `DISABLED` also clears the consecutive-failure counter, so an auto-disabled endpoint gets a clean slate.
+ *
+ * The signing secret is untouched by an update, and is not part of this response.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ patch: operations["v1UpdateWebhook"];
+ trace?: never;
+ };
+ "/api/v1/webhooks/{id}/rotate-secret": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/campaigns/{id}/send. */
- CampaignV1Send: {
- /**
- * Format: date-time
- * @description RFC 3339 timestamp, strictly in the future. A numeric UTC offset (`+02:00`) is accepted as well as `Z`. Omit to start sending immediately.
- */
- scheduled_for?: string;
+ get?: never;
+ put?: never;
+ /**
+ * Rotate a webhook signing secret
+ * @description Mint a fresh signing secret. The new plaintext is returned EXACTLY ONCE, here — no endpoint reads it back, so a secret you lose is replaced by rotating again rather than recovered.
+ *
+ * Rotation overlaps on purpose. The outgoing secret keeps verifying until `previous_secret_expires_at`, and every delivery inside that window carries BOTH signatures in the `webhook-signature` header — so you can deploy the new secret to your verifier whenever you like without dropping an in-flight event. Rotating twice inside the window discards the older secret: only one previous secret is ever live.
+ *
+ * `url`, `event_types` and `status` are unchanged.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["v1RotateWebhookSecret"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Materialized delivery and engagement counters for one campaign. */
- CampaignV1Stats: {
- bounce_rate: number;
- bounced: number;
- click_rate: number;
- clicked: number;
- delivered: number;
- delivery_rate: number;
- open_rate: number;
- opened: number;
- sent: number;
- total_recipients: number;
+ /**
+ * List workflows
+ * @description Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * Unlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1ListWorkflows"];
+ put?: never;
+ /**
+ * Create a workflow
+ * @description Creates a workflow with its single trigger step. `trigger_type` defaults to `EVENT`, which requires `event_name`; `SCHEDULE` takes `interval_ms` and `MANUAL` is started only by `POST /api/v1/workflows/{id}/executions`.
+ *
+ * Pass `sequence` to create the steps at the same time, as a LINEAR chain behind the trigger. Anything with a branch is `PUT /api/v1/workflows/{id}/graph`. Without a sequence the workflow holds only its trigger and stays inert until it has steps to run, which is why it is created disabled.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CreateWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/executions/{execution_id}/cancel": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for PATCH /api/v1/campaigns/{id}. All fields optional. */
- CampaignV1Update: {
- audience_condition?: components["schemas"]["FilterConditionV1"];
- /** @enum {string} */
- audience_type?: "ALL" | "FILTERED" | "SEGMENT";
- body?: string;
- description?: string;
- /**
- * Format: email
- * @description Sender address. Its domain must be verified for this project.
- */
- from?: string;
- from_name?: string | null;
- name?: string;
- /** Format: email */
- reply_to?: string | null;
- /** Format: uuid */
- segment_id?: string;
- subject?: string;
- /** @enum {string} */
- type?: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ get?: never;
+ put?: never;
+ /**
+ * Cancel a workflow execution
+ * @description Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CancelWorkflowExecution"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description A subscriber/contact within a project. */
- Contact: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- customFields?: {
- [key: string]: unknown;
- } | null;
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
- /** Format: uuid */
- projectId: string;
- subscribed: boolean;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
+ /**
+ * Retrieve a workflow
+ * @description The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflow"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a workflow
+ * @description Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ delete: operations["v1DeleteWorkflow"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a workflow
+ * @description Sparse update — omitted fields are left unchanged.
+ *
+ * Two state rules apply: the trigger (`trigger_type`/`event_name`/`interval_ms`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ *
+ * `sequence` REPLACES every non-trigger step with a linear chain and is likewise refused while executions are running. Omit it to leave the graph untouched.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ patch: operations["v1UpdateWorkflow"];
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/clone": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Bulk create up to 1000 contacts. */
- ContactBulkCreateBody: {
- contacts: components["schemas"]["CreateContact"][];
+ get?: never;
+ put?: never;
+ /**
+ * Clone a workflow
+ * @description Copies a workflow and its whole graph as a new workflow. The copy is always created disabled, whatever the original was: a clone exists to be reviewed, and one that started live would match the same trigger events as its original from the moment it appeared.
+ *
+ * Server-side rather than a read-then-write, so the copy is taken from one consistent read of the source.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CloneWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/executions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each). */
- ContactBulkDeleteBody: {
- emails?: string[];
- ids?: string[];
+ /**
+ * List a workflow's executions
+ * @description One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1ListWorkflowExecutions"];
+ put?: never;
+ /**
+ * Start a workflow for a contact
+ * @description Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.
+ *
+ * 409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1StartWorkflowExecution"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/graph": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Cursor-paginated list of contacts. */
- ContactListResponse: {
- data: {
- data: components["schemas"]["Contact"][];
- hasMore: boolean;
- /** @description Cursor for the next page, or null on the last page. */
- nextCursor: string | null;
- total: number;
- };
- /** @enum {boolean} */
- success: true;
+ /**
+ * Retrieve a workflow's step graph
+ * @description Every step in the workflow, including its `TRIGGER` entry node, and every directed transition between them. `version` is the workflow's version at the time of the read — a different number on a later read means somebody edited the graph in between.
+ *
+ * A step's `config` is returned exactly as stored, camelCase keys and all, rather than projected into the snake_case used everywhere else on this API. That is deliberate: the same document is authored by the visual editor, and renaming its keys on the way out would mean any key this API did not know about was silently dropped on the way back in.
+ *
+ * The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path: read a graph, change a step, send it back. Step and transition ids are yours to choose on a write, which is what makes that round trip a no-op rather than a rebuild.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflowGraph"];
+ /**
+ * Replace a workflow's step graph
+ * @description Replaces the whole graph in one transaction, because a graph is only meaningful as nodes PLUS the edges between them — an edit that could apply half of it would leave steps pointing at steps that no longer exist.
+ *
+ * A step whose id you send is kept and updated in place; a fresh uuid creates one; an id you omit deletes that step and its run history. Exactly one step must be a `TRIGGER`, every transition must name steps present in the same document, and a step may not point at itself.
+ *
+ * Refused with 409 while the workflow has running executions: those runs are standing on the steps being replaced. Pause the workflow (`POST /api/v1/workflows/{id}/pause`) first.
+ *
+ * The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path: read a graph, change a step, send it back. Step and transition ids are yours to choose on a write, which is what makes that round trip a no-op rather than a rebuild.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ put: operations["v1ReplaceWorkflowGraph"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/pause": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateApiKeyBody: {
- /** Format: uuid */
- domainId?: string | null;
- name: string;
- /** @enum {string} */
- permission?: "FULL" | "SENDING_ONLY";
- /** @description The explicit grant the new key will carry. Omitted ⇒ materialised from `permission`. A `SENDING_ONLY` key may carry only `emails:send`. */
- scopes?: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test")[];
+ get?: never;
+ put?: never;
+ /**
+ * Pause a workflow and cancel its running executions
+ * @description Disables the workflow and cancels every `RUNNING`/`WAITING` execution inside it.
+ *
+ * `PATCH { "enabled": false }` stops NEW runs starting and leaves every in-flight contact walking the graph — the next delay still expires, the next email still sends. Pausing does both, and reports how many runs it stopped.
+ *
+ * Cancelling is terminal: `resume` re-opens the workflow to new runs, it does not put the cancelled contacts back where they were.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1PauseWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/resume": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/mailboxes/:id/app-passwords. */
- CreateAppPassword: {
- name: string;
- /**
- * @default [
- * "imap",
- * "smtp"
- * ]
- */
- scopes: ("imap" | "smtp")[];
+ get?: never;
+ put?: never;
+ /**
+ * Resume a paused workflow
+ * @description Re-enables the workflow so its trigger matches again. `cancelled_executions` is always 0 here — resuming starts nothing and stops nothing.
+ *
+ * Refused with 422 while any step is still unconfigured, the same rule `PATCH { "enabled": true }` enforces: an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1ResumeWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/contacts and /api/contacts/upsert. */
- CreateContact: {
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- customFields?: {
- [key: string]: unknown;
- };
- /** Format: email */
- email: string;
- /** @default true */
- subscribed: boolean;
+ /**
+ * Retrieve workflow statistics
+ * @description Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.
+ *
+ * The workflow's own `name`, `enabled`, `trigger_type` and `step_count` travel with the counts, because the counts alone are ambiguous: no running executions means one thing on an enabled workflow and another on a paused one.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflowStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/verify": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateMailboxBody: {
- displayName?: string;
- /**
- * Format: uuid
- * @description A VERIFIED domain belonging to this project.
- */
- domainId: string;
- /** @description The part before the `@`, e.g. `support`. Lowercased server-side. */
- localPart: string;
+ get?: never;
+ put?: never;
+ /**
+ * Validate an email address
+ * @description Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.
+ */
+ post: operations["verifyEmailAddress"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List user webhooks
+ * @description List all user-managed outbound webhooks for the auth'd project (secrets are not returned).
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["listWebhooks"];
+ put?: never;
+ /**
+ * Create a webhook
+ * @description Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["createWebhook"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get a webhook
+ * @description Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["getWebhook"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a webhook
+ * @description Hard-delete a webhook. Cascades to all WebhookCall rows.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ delete: operations["deleteWebhook"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a webhook
+ * @description Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ patch: operations["updateWebhook"];
+ trace?: never;
+ };
+ "/api/webhooks/{id}/calls": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List recent webhook calls
+ * @description Cursor-paginated list of recent delivery attempts for a single webhook.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["listWebhookCalls"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks/{id}/rotate-secret": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Rotate the webhook signing secret
+ * @description Generate a new shared secret. Returns the new plaintext secret exactly once.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["rotateWebhookSecret"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+interface components {
+ schemas: {
+ /** @description Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region. `stream` assigns the identity to transactional or marketing traffic at creation; omit it to leave the identity serving both. */
+ AddDomainBody: {
+ domain: string;
+ /** Format: uuid */
+ projectId?: string;
/**
- * Format: uuid
- * @description Defaults to the project the credential resolves to. Naming a different one is refused.
+ * @description Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region.
+ * @enum {string}
*/
- projectId?: string;
- /** @description NOT IMPLEMENTED — sending any value answers 400. */
- quotaBytes?: number;
+ region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ stream?: components["schemas"]["SendingStream"];
+ /** @description Make this the project's default identity for `stream`. Requires `stream`. Setting it demotes whichever identity held it. */
+ streamDefault?: boolean;
};
- /** @description Body for POST /api/templates. */
- CreateTemplate: {
- body: string;
- description?: string;
- /** Format: email */
- from: string;
- fromName?: string | null;
- name: string;
+ /** @description Body for POST /api/suppression — manually add an email to the suppression list. */
+ AddSuppression: {
/** Format: email */
- replyTo?: string | null;
- subject: string;
+ email: string;
/**
- * @default MARKETING
+ * @default MANUAL
* @enum {string}
*/
- type: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
};
- /** @description Body for POST /api/webhooks — register a user webhook for one or more events. */
- CreateWebhook: {
- eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** Format: uri */
- url: string;
+ /** @description Campaign counters and engagement over the window. */
+ AnalyticsCampaignStatsV1: {
+ /** @description Campaigns in DRAFT or SCHEDULED. */
+ active: number;
+ average_click_rate: number;
+ /** @description Percentage, one decimal place. */
+ average_open_rate: number;
+ completed: number;
+ total: number;
+ window: components["schemas"]["AnalyticsWindowV1"];
};
- /** @description A sending domain registered with SES. */
- Domain: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- dkim?: {
- name: string;
- type: string;
- value: string;
+ /** @description Daily email counters across the window. Every day in range is present, zero-filled. */
+ AnalyticsTimeseriesV1: {
+ data: {
+ bounces: number;
+ clicks: number;
+ /** Format: date-time */
+ date: string;
+ delivered: number;
+ emails: number;
+ opens: number;
+ }[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description Sent campaigns ranked by open rate. */
+ AnalyticsTopCampaignsV1: {
+ data: {
+ click_rate: number;
+ clicked: number;
+ /** Format: uuid */
+ id: string;
+ open_rate: number;
+ opened: number;
+ sent: number;
+ subject: string;
}[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description The time range this response was computed over, after the 90-day clamp. */
+ AnalyticsWindowV1: {
+ /** Format: date-time */
+ from: string;
+ /** Format: date-time */
+ to: string;
+ };
+ /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
+ ApiKey: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** Format: uuid */
+ domainId: string | null;
/** Format: uuid */
id: string;
- /** @description Custom MAIL FROM subdomain SES has on record (normally `sendly.`). */
- mailFromDomain?: string | null;
+ /** @description Last 4 characters of the token — the only fragment of the secret that survives creation. */
+ lastFour: string;
/**
- * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
- * @enum {string|null}
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ lastUsedAt: string | null;
+ /**
+ * @description The coarse preset the key was minted under. It picks the token prefix (`sk_` vs `pk_`) and the rate-limit window, and it is the key's authority ONLY while `scopes` is empty — which is true just of keys minted before scopes existed. Read `scopes` to learn what a key can do.
+ * @enum {string}
*/
- mailFromStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ legacyGrantPreset: "FULL" | "SENDING_ONLY";
+ /**
+ * @description Where mail sent with this key may go. `LIVE` sends from your verified domains. `TEST` can only send from this project's sandbox address, whose recipient list is your own verified account addresses, so a test key cannot reach a customer whatever it is asked to do. Fixed at creation.
+ * @enum {string}
+ */
+ mode: "LIVE" | "TEST";
name: string;
/** Format: uuid */
projectId: string;
- region?: string | null;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description Set once the key is revoked. Revoked keys are NOT filtered out of list/get responses.
*/
- updatedAt: string;
- verified: boolean;
+ revokedAt: string | null;
+ /** @description The explicit scope grant this key carries. Empty on a key minted before this column existed — that row's grant is derived from `legacyGrantPreset` instead at request time. */
+ scopes: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test" | "deliverability:read" | "mailboxes:send" | "validation:read" | "validation:write" | "topics:read" | "topics:write" | "lists:read" | "lists:write")[];
};
- /** @description List of all domains for the auth'd project. */
- DomainListResponse: {
- data: components["schemas"]["Domain"][];
+ /** @description Every API key on the project, including revoked ones — filter on `revokedAt` for live keys. */
+ ApiKeyListResponse: {
+ data: components["schemas"]["ApiKey"][];
/** @enum {boolean} */
success: true;
};
- /** @description Outcome of a verification check against SES. */
- DomainVerificationStatus: {
- dkim?: {
- name: string;
- type: string;
- value: string;
- }[];
- mailFromDomain?: string | null;
- /**
- * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
- * @enum {string|null}
- */
- mailFromStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
- mxRecords?: string[];
- verified: boolean;
- };
- /** @description A sent (or queued) transactional email. */
- Email: {
+ /** @description An IMAP/SMTP credential for one mailbox, described but never reproduced. */
+ AppPassword: {
+ /** Format: date-time */
+ createdAt: string;
+ /** Format: uuid */
+ id: string;
+ /** @description The last four characters of the secret — enough to tell two credentials apart, and nothing more. */
+ lastFour: string;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description Null until a mail client has authenticated with it at least once.
*/
- createdAt: string;
- error?: string | null;
- from: string;
+ lastUsedAt: string | null;
+ /** @description What the credential is for, e.g. `Thunderbird on my laptop`. */
+ name: string;
+ /** @description Which protocols this password may authenticate. `imap` reads, `smtp` sends. */
+ scopes: ("imap" | "smtp")[];
+ };
+ /** @description A newly created app password, handed over as a one-time link rather than as a secret. */
+ AppPasswordReveal: {
/** Format: uuid */
id: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- status: "PENDING" | "SENT" | "DELIVERED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED";
- subject: string;
- tags: string[];
- to: string;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description When the link stops working. Five minutes after creation; the password itself does not expire.
*/
- updatedAt: string;
- };
- /** @description Single email with its events. */
- EmailGetResponse: {
- data: components["schemas"]["Email"];
- /** @enum {boolean} */
- success: true;
- };
- /** @description Cursor-paginated list of emails. */
- EmailListResponse: {
- data: components["schemas"]["Email"][];
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A single-use link that shows the password once, in a browser. Opening it requires a signed-in Sendly session belonging to a project admin — the connection that created the password cannot open it, and the second attempt to open it fails whoever makes it.
+ */
+ revealUrl: string;
};
- /** @description Receipt for a sandbox test send. */
- EmailTestV1: {
+ /** @description Body for PATCH /api/domains/{id}. */
+ AssignDomainStream: {
/**
* Format: email
- * @description This project's sandbox sender — resolved server-side, never from the body.
+ * @description The address a send on this stream uses when it names none. Must be on this identity's own host — a default pointing elsewhere would go out unsigned by the name in the From header.
*/
- from: string;
+ defaultFromAddress?: string | null;
/**
- * Format: uuid
- * @description The Email row this send created.
+ * @description Which traffic this identity carries. `null` unassigns it, returning it to serving every stream and clearing its default flag and default address.
+ * @enum {string|null}
*/
+ stream?: "TRANSACTIONAL" | "MARKETING" | null;
+ /** @description Make this the project's default identity for its stream, demoting whichever held it. */
+ streamDefault?: boolean;
+ };
+ /** @description Per-row result in a batch send response. */
+ BatchEntryResult: {
+ data?: components["schemas"]["SendEmailData"];
+ error?: {
+ code: string;
+ message: string;
+ };
+ index: number;
+ /** @enum {string} */
+ status: "ok" | "error";
+ };
+ /** @description Batch send wrapper. Up to 100 entries. */
+ BatchSendBody: {
+ emails: components["schemas"]["SendEmail"][];
+ };
+ /** @description Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200. */
+ BatchSendResponse: {
+ data: components["schemas"]["BatchEntryResult"][];
+ success: boolean;
+ };
+ /** @description A campaign as exposed on the v1 API. */
+ CampaignV1: {
+ /** @enum {string} */
+ audience_type: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ /** Format: date-time */
+ created_at: string;
+ /** Format: uuid */
id: string;
+ /** Format: uuid */
+ list_id: string | null;
+ name: string;
+ /** Format: date-time */
+ scheduled_at: string | null;
+ /** Format: date-time */
+ sent_at: string | null;
+ stats: {
+ bounced: number;
+ clicked: number;
+ delivered: number;
+ opened: number;
+ sent: number;
+ total_recipients: number;
+ };
+ /** @enum {string} */
+ status: "DRAFT" | "SCHEDULED" | "SENDING" | "PAUSED" | "SENT" | "CANCELLED";
+ subject: string;
+ /** Format: uuid */
+ topic_id: string | null;
+ };
+ /** @description Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, `audience_condition` when it is `FILTERED`, and `list_id` when it is `LIST`. */
+ CampaignV1Create: {
+ audience_condition?: components["schemas"]["FilterConditionV1"];
/**
- * @description Always true. It is here so a model relaying this result cannot describe a test send as a real one: the message came from the shared sandbox domain and could only reach the project owner's own inbox.
- * @enum {boolean}
+ * @description `ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`. `LIST` — the CONFIRMED members of `list_id`, which is the only audience that honours double opt-in: a member who never confirmed, or who unsubscribed, is not mailed.
+ * @enum {string}
*/
- sandbox: true;
+ audience_type: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ body: string;
+ description?: string;
/**
- * @description Delivery status at the moment of the response — `PENDING` for a send still queued.
+ * @default MARKETING
* @enum {string}
*/
- status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
/**
* Format: email
- * @description The recipient the message was queued for.
+ * @description Sender address. Its domain must be verified for this project.
*/
- to: string;
- };
- /** @description Receipt for a single transactional send. */
- EmailV1: {
+ from: string;
+ from_name?: string | null;
/**
- * Format: email
- * @description The sender actually used. Worth reading rather than assuming: it is resolved server-side and may come from the template when the request named none.
+ * Format: uuid
+ * @description Required when `audience_type` is `LIST`. Only that list's CONFIRMED members receive the campaign.
*/
- from: string;
+ list_id?: string;
+ name: string;
+ /** Format: email */
+ reply_to?: string | null;
+ /** Format: uuid */
+ segment_id?: string;
+ subject: string;
/**
* Format: uuid
- * @description The Email row this send created. Quote it in support requests.
+ * @description The subject this campaign is about. A contact who unsubscribed from the topic is excluded WHATEVER the audience type — a topic opt-out is a standing answer, not an audience filter, so it cannot be routed around by selecting a different audience. Ignored for `TRANSACTIONAL` campaigns, which a marketing preference does not cancel.
*/
+ topic_id?: string | null;
+ };
+ /** @description Acknowledgement that a campaign was deleted. */
+ CampaignV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
id: string;
+ };
+ /** @description A campaign recipient whose send did not complete. */
+ CampaignV1Failure: {
+ /** Format: uuid */
+ contact_id: string;
+ /** @description The recipient the send was for. */
+ email: string;
+ /** Format: date-time */
+ failed_at: string;
/**
- * @description Delivery status at the moment of the response — `PENDING` for a send the worker has not picked up yet, which is the usual answer. Later states arrive via webhooks, not here.
- * @enum {string}
+ * Format: uuid
+ * @description Ledger row id. Pass the last one as `after` to page.
*/
- status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ id: string;
+ reason: string | null;
+ };
+ /** @description Cursor-paginated list of a campaign's failed sends. */
+ CampaignV1FailureList: {
+ data: components["schemas"]["CampaignV1Failure"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ /** @description Every FAILED row on this campaign, not just this page. */
+ total: number;
+ };
+ /** @description Cursor-paginated list of campaigns. */
+ CampaignV1List: {
+ data: components["schemas"]["CampaignV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Acknowledgement that a retry of a campaign's failed sends began. */
+ CampaignV1RetryFailed: {
+ /** Format: uuid */
+ id: string;
+ /** @description How many FAILED rows the retry walk was started for, counted when it was queued. */
+ queued: number;
+ };
+ /** @description Body for POST /api/v1/campaigns/{id}/send. */
+ CampaignV1Send: {
/**
- * Format: email
- * @description The recipient the message was queued for.
+ * Format: date-time
+ * @description RFC 3339 timestamp, strictly in the future. A numeric UTC offset (`+02:00`) is accepted as well as `Z`. Omit to start sending immediately.
*/
- to: string;
+ scheduled_for?: string;
};
- /** @description Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`. */
- Error: {
- error: {
- code: string;
+ /** @description Materialized delivery and engagement counters for one campaign. */
+ CampaignV1Stats: {
+ bounce_rate: number;
+ bounced: number;
+ click_rate: number;
+ clicked: number;
+ delivered: number;
+ delivery_rate: number;
+ open_rate: number;
+ opened: number;
+ sent: number;
+ total_recipients: number;
+ };
+ /** @description Body for PATCH /api/v1/campaigns/{id}. All fields optional. */
+ CampaignV1Update: {
+ audience_condition?: components["schemas"]["FilterConditionV1"];
+ /** @enum {string} */
+ audience_type?: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ body?: string;
+ description?: string;
+ /** @enum {string} */
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /**
+ * Format: email
+ * @description Sender address. Its domain must be verified for this project.
+ */
+ from?: string;
+ from_name?: string | null;
+ /**
+ * Format: uuid
+ * @description Required when `audience_type` is `LIST`. Only that list's CONFIRMED members receive the campaign.
+ */
+ list_id?: string;
+ name?: string;
+ /** Format: email */
+ reply_to?: string | null;
+ /** Format: uuid */
+ segment_id?: string;
+ subject?: string;
+ /**
+ * Format: uuid
+ * @description The subject this campaign is about. A contact who unsubscribed from the topic is excluded WHATEVER the audience type — a topic opt-out is a standing answer, not an audience filter, so it cannot be routed around by selecting a different audience. Ignored for `TRANSACTIONAL` campaigns, which a marketing preference does not cancel.
+ */
+ topic_id?: string | null;
+ };
+ /** @description Body for POST /api/mailboxes/{id}/messages — a new outbound message from a hosted mailbox. */
+ ComposeMailboxMessage: {
+ bcc?: string[];
+ body: string;
+ cc?: string[];
+ subject: string;
+ to: string[];
+ };
+ /** @description A subscriber/contact within a project. */
+ Contact: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ customFields?: {
+ [key: string]: unknown;
+ } | null;
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ /** Format: uuid */
+ projectId: string;
+ subscribed: boolean;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Bulk create up to 1000 contacts. */
+ ContactBulkCreateBody: {
+ contacts: components["schemas"]["CreateContact"][];
+ };
+ /** @description Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each). */
+ ContactBulkDeleteBody: {
+ emails?: string[];
+ ids?: string[];
+ };
+ /** @description Cursor-paginated list of contacts. */
+ ContactListResponse: {
+ data: {
+ data: components["schemas"]["Contact"][];
+ hasMore: boolean;
+ /** @description Cursor for the next page, or null on the last page. */
+ nextCursor: string | null;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Everything this contact has said about what they want. `subscribed` on a topic already folds in `default_opt_in`, so a contact who has never answered still reads correctly. */
+ ContactTopicPreferencesV1: {
+ contact_id: string;
+ /** @description The global marketing opt-out, which OUTRANKS every topic. False means no marketing reaches this contact whatever the topics below say. */
+ subscribed: boolean;
+ topics: {
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ pending: boolean;
+ /** @description The EFFECTIVE answer: what the send path concludes for this contact today. */
+ subscribed: boolean;
+ topic_id: string;
+ }[];
+ };
+ /** @description A contact as exposed on the v1 API. */
+ ContactV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ email: string;
+ /** Format: uuid */
+ id: string;
+ subscribed: boolean;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/contacts. */
+ ContactV1Create: {
+ /** @description Arbitrary JSON stored on the contact and available to templates as `{{ variables }}`. */
+ custom_fields?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ /** Format: email */
+ email: string;
+ /** @default true */
+ subscribed: boolean;
+ };
+ /** @description Acknowledgement that a contact was deleted. */
+ ContactV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of contacts. */
+ ContactV1List: {
+ data: components["schemas"]["ContactV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/contacts/{id}. `email` is deliberately absent: an address is the contact's identity on this API, and changing it in place would silently rewrite what every earlier send was addressed to. Create the new address instead. */
+ ContactV1Update: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ subscribed?: boolean;
+ };
+ CreateApiKeyBody: {
+ /** Format: uuid */
+ domainId?: string | null;
+ /** @enum {string} */
+ legacyGrantPreset?: "FULL" | "SENDING_ONLY";
+ /**
+ * @description `LIVE` (default) or `TEST`. A test key can only send from the project's sandbox address, which accepts only the project's own verified account addresses as recipients, so it cannot reach a customer. Its sends are real sends down the same pipeline and are marked `sentInTestMode`. Fixed at creation.
+ * @enum {string}
+ */
+ mode?: "LIVE" | "TEST";
+ name: string;
+ /** @description The explicit grant the new key will carry. Omitted ⇒ materialised from `legacyGrantPreset`. A `SENDING_ONLY` key may carry only `emails:send`. */
+ scopes?: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test" | "deliverability:read" | "mailboxes:send" | "validation:read" | "validation:write" | "topics:read" | "topics:write" | "lists:read" | "lists:write")[];
+ };
+ /** @description Body for POST /api/mailboxes/:id/app-passwords. */
+ CreateAppPassword: {
+ name: string;
+ /**
+ * @default [
+ * "imap",
+ * "smtp"
+ * ]
+ */
+ scopes: ("imap" | "smtp")[];
+ };
+ /** @description Body for POST /api/contacts and /api/contacts/upsert. */
+ CreateContact: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ customFields?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ /** @default true */
+ subscribed: boolean;
+ };
+ CreateMailboxBody: {
+ displayName?: string;
+ /**
+ * Format: uuid
+ * @description A VERIFIED domain belonging to this project.
+ */
+ domainId: string;
+ /** @description The part before the `@`, e.g. `support`. Lowercased server-side. */
+ localPart: string;
+ /**
+ * Format: uuid
+ * @description Defaults to the project the credential resolves to. Naming a different one is refused.
+ */
+ projectId?: string;
+ /** @description NOT IMPLEMENTED — sending any value answers 400. */
+ quotaBytes?: number;
+ };
+ /** @description Body for POST /api/snippets. */
+ CreateSnippet: {
+ body: string;
+ description?: string | null;
+ name: string;
+ };
+ /** @description Body for POST /api/templates. */
+ CreateTemplate: {
+ body: string;
+ description?: string;
+ /**
+ * @default MARKETING
+ * @enum {string}
+ */
+ emailCategory: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from: string;
+ fromName?: string | null;
+ name: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject: string;
+ };
+ /** @description Body for POST /api/webhooks — register a user webhook for one or more events. */
+ CreateWebhook: {
+ eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uri */
+ url: string;
+ };
+ /** @description A composed answer to why mail from one domain may not be arriving: its DNS identity, the project's recent delivery outcomes, one address's suppression state, and the findings drawn from them. */
+ DeliverabilityDiagnosisV1: {
+ address: string | null;
+ /** Format: date-time */
+ checked_at: string;
+ domain: string;
+ /** @description What is wrong, worst first. An empty array means nothing here explains a delivery problem. */
+ findings: components["schemas"]["DeliverabilityFindingV1"][];
+ identity: components["schemas"]["DeliverabilityIdentityV1"];
+ recent_delivery: components["schemas"]["DeliverabilityRecentDeliveryV1"];
+ suppression: components["schemas"]["DeliverabilitySuppressionV1"];
+ };
+ /**
+ * @description `blocking`: mail from this domain cannot be delivered as configured. `degraded`: it delivers, but inbox placement or sender reputation is at risk. `info`: worth knowing, nothing to fix.
+ * @enum {string}
+ */
+ DeliverabilityFindingSeverityV1: "blocking" | "degraded" | "info";
+ /** @description One diagnosed problem, with its fix. */
+ DeliverabilityFindingV1: {
+ /** @description Stable identifier for this finding, e.g. `domain_not_verified`. Branch on this, not on `summary`. */
+ code: string;
+ /** @description What to do about it. */
+ remedy: string;
+ severity: components["schemas"]["DeliverabilityFindingSeverityV1"];
+ /** @description What is wrong, in one sentence. */
+ summary: string;
+ };
+ /** @description The sending identity's DNS health, as last refreshed. */
+ DeliverabilityIdentityV1: {
+ /**
+ * @description DKIM signing. This is the one that decides whether Sendly will send from the domain at all.
+ * @enum {string|null}
+ */
+ dkim_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * @description The DMARC policy published at `_dmarc.`.
+ * @enum {string|null}
+ */
+ dmarc_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * Format: date-time
+ * @description When the DNS refresh job last looked. These statuses are a CACHE, not a live lookup.
+ */
+ last_checked_at: string | null;
+ mail_from_domain: string | null;
+ /** @description Raw SES `CustomMailFromStatus` (`Pending`/`Success`/`Failed`/`TemporaryFailure`), or `NotConfigured`. `Failed` means SES silently fell back to `amazonses.com`, which is why the value is reported rather than folded into a boolean. */
+ mail_from_domain_status: string | null;
+ /**
+ * @description Inbound receiving only. Null unless the domain has receiving enabled.
+ * @enum {string|null}
+ */
+ mx_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description Whether this project has a domain record at all. False makes every other field null. */
+ registered: boolean;
+ /**
+ * @description SPF alignment for the sending identity.
+ * @enum {string|null}
+ */
+ spf_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ verified: boolean;
+ };
+ /** @description Delivery outcomes over the requested window. */
+ DeliverabilityRecentDeliveryV1: {
+ /** @description Bounced ÷ sent (0–1), or null when nothing was sent in the window. */
+ bounce_rate: number | null;
+ bounced: number;
+ complained: number;
+ complaint_rate: number | null;
+ delivered: number;
+ failed: number;
+ /**
+ * @description PROJECT-WIDE, not per-domain, and said so rather than implied. `Email` has no sending-domain column, so narrowing these counters to one domain would mean a scan over every row the project has ever sent. A project that sends from one domain — most of them — can read these as that domain's.
+ * @enum {string}
+ */
+ scope: "project";
+ sent: number;
+ window_days: number;
+ };
+ /** @description Null unless the request named an `address`. */
+ DeliverabilitySuppressionV1: {
+ /** @enum {string|null} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE" | null;
+ /** @enum {string|null} */
+ source: "SES_WEBHOOK" | "API" | "DASHBOARD" | null;
+ suppressed: boolean;
+ /** Format: date-time */
+ suppressed_at: string | null;
+ } | null;
+ /** @description One DMARC aggregate (RUA) report. */
+ DmarcReportV1: {
+ fail_count: number;
+ id: string;
+ /** @description The reporting receiver, e.g. `google.com`. */
+ org_name: string;
+ /** @description Messages DMARC-ALIGNED (SPF or DKIM aligned and passing), from `policy_evaluated`. Not the raw auth results: a message can pass SPF for a domain that is not the one in its From header, which is the case DMARC exists to catch. */
+ pass_count: number;
+ /** @description The domain of yours the report is about. */
+ policy_domain: string;
+ /** Format: date-time */
+ range_begin: string;
+ /** Format: date-time */
+ range_end: string;
+ /** Format: date-time */
+ received_at: string;
+ /** @description The receiver's own id for this report. */
+ report_id: string;
+ /** @description Per-sending-source rows, as the receiver reported them. */
+ sources: {
+ count: number;
+ disposition: string;
+ dkim: string;
+ header_from: string;
+ source_ip: string;
+ spf: string;
+ }[];
+ total_count: number;
+ };
+ /** @description Cursor-paginated DMARC aggregate reports, newest window first. */
+ DmarcReportV1List: {
+ data: components["schemas"]["DmarcReportV1"][];
+ has_more: boolean;
+ /** @description Whether this deployment has a DMARC report intake mailbox configured. When `false` no report can ever arrive, so an empty `data` means the feature is off rather than that your domains are clean — the two are otherwise indistinguishable. */
+ intake_configured: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A sending identity: one domain registered with SES, with its own DKIM keys, its own MAIL FROM and its own reputation. */
+ Domain: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** @description The address a send on this stream uses when it names none. Always on this identity's own host. */
+ defaultFromAddress?: string | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ dkimStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description SES DKIM tokens to publish as CNAME records before the domain can verify. */
+ dkimTokens?: string[] | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ dmarcStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description The bare domain, e.g. `mail.acme.com`. */
+ domain: string;
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ lastHealthCheckAt?: string | null;
+ /** @description Custom MAIL FROM subdomain SES has on record (normally `sendly.`). */
+ mailFromDomain?: string | null;
+ /**
+ * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
+ * @enum {string|null}
+ */
+ mailFromDomainStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ /** Format: uuid */
+ projectId: string;
+ /** @description Whether inbound mail for this domain is routed to Sendly mailboxes. */
+ receivingEnabled: boolean;
+ region?: string | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ spfStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * @description Which traffic this identity carries. `null` means unassigned, and an unassigned identity carries every stream — the behaviour of every domain added before per-stream identities. A send whose stream does not match an ASSIGNED identity is refused.
+ * @enum {string|null}
+ */
+ stream?: "TRANSACTIONAL" | "MARKETING" | null;
+ /** @description Whether this is the project's default identity for its stream — the one a send picks when it names no from-address. At most one per (project, stream). */
+ streamDefault?: boolean;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ verified: boolean;
+ };
+ /** @description List of all domains for the auth'd project. */
+ DomainListResponse: {
+ data: components["schemas"]["Domain"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A sending domain as exposed on the v1 API. */
+ DomainV1: {
+ /** Format: date-time */
+ created_at: string;
+ default_from_address: string | null;
+ dkim_verified: boolean;
+ domain: string;
+ /** Format: uuid */
+ id: string;
+ mail_from_domain: string | null;
+ /** @description SES's CustomMailFromStatus for `mail_from_domain` — the subdomain that carries the bounce path, NOT the status of any From address. */
+ mail_from_domain_status: string | null;
+ region: string | null;
+ stream: components["schemas"]["SendingStream"] & (string | null);
+ stream_default: boolean;
+ /** Format: date-time */
+ updated_at: string;
+ verified: boolean;
+ };
+ /** @description Body for POST /api/v1/domains. */
+ DomainV1Create: {
+ domain: string;
+ /**
+ * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
+ * @enum {string}
+ */
+ region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ stream?: components["schemas"]["SendingStream"] & unknown;
+ /** @description Make this the project's default identity for `stream`. Requires `stream`. */
+ stream_default?: boolean;
+ };
+ /** @description Acknowledgement that a sending domain was removed. */
+ DomainV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of sending domains. */
+ DomainV1List: {
+ data: components["schemas"]["DomainV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Outcome of a verification check against SES. */
+ DomainVerificationStatus: {
+ /** @enum {string} */
+ dkimStatus: "VERIFIED" | "PENDING" | "FAILED";
+ /** @enum {string} */
+ dmarcStatus: "VERIFIED" | "FAILED" | "NOT_CHECKED";
+ domain: string;
+ mailFromDomain: string | null;
+ /**
+ * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
+ * @enum {string|null}
+ */
+ mailFromDomainStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ /** @enum {string} */
+ spfStatus: "VERIFIED" | "FAILED" | "NOT_CHECKED";
+ /** @description Raw SES DKIM verification status, e.g. `Success` or `Pending`. */
+ status: string;
+ /** @description DKIM tokens SES still has to report. Absent once verification has resolved. */
+ tokens?: string[];
+ verified: boolean;
+ };
+ /** @description Body for POST /api/mailboxes/{id}/drafts — ask for help writing, never for sending. */
+ DraftMailboxMessage: {
+ brief?: string;
+ draft?: string;
+ instruction?: string;
+ /** @enum {string} */
+ mode: "draft" | "rewrite" | "subject";
+ recipientContext?: string;
+ senderAddress?: string;
+ /** @enum {string} */
+ tone?: "friendly" | "neutral" | "formal" | "apologetic" | "direct";
+ };
+ /** @description A sent (or queued) transactional email. */
+ Email: {
+ /**
+ * Format: date-time
+ * @description Bounced, or null.
+ */
+ bouncedAt: string | null;
+ /**
+ * Format: date-time
+ * @description First click, or null.
+ */
+ clickedAt: string | null;
+ /** @description Total clicks recorded. */
+ clicks: number;
+ /**
+ * Format: date-time
+ * @description Spam complaint, or null.
+ */
+ complainedAt: string | null;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description Accepted by the recipient's server, or null.
+ */
+ deliveredAt: string | null;
+ error?: string | null;
+ from: string;
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description First open, or null.
+ */
+ openedAt: string | null;
+ /** @description Total opens recorded. */
+ opens: number;
+ /** Format: uuid */
+ projectId: string;
+ /**
+ * Format: date-time
+ * @description Handed to the provider, or null.
+ */
+ sentAt: string | null;
+ status: components["schemas"]["EmailDeliveryStatus"];
+ subject: string;
+ tags: string[];
+ to: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /**
+ * @description Delivery lifecycle of the message. Engagement is reported separately.
+ * @enum {string}
+ */
+ EmailDeliveryStatus: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /** @description One email and its delivery history. */
+ EmailDetailResponse: {
+ data: components["schemas"]["EmailWithEvents"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description One transition in a message's delivery history. */
+ EmailEvent: {
+ /** Format: uuid */
+ id: string;
+ status: components["schemas"]["EmailDeliveryStatus"];
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @description Cursor-paginated list of emails. */
+ EmailListResponse: {
+ data: components["schemas"]["Email"][];
+ nextCursor?: string | null;
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A single email. */
+ EmailResponse: {
+ data: components["schemas"]["Email"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Receipt for a sandbox test send. */
+ EmailTestV1: {
+ /**
+ * Format: email
+ * @description This project's sandbox sender — resolved server-side, never from the body.
+ */
+ from: string;
+ /**
+ * Format: uuid
+ * @description The Email row this send created.
+ */
+ id: string;
+ /**
+ * @description Always true. It is here so a model relaying this result cannot describe a test send as a real one: the message came from the shared sandbox domain and could only reach the project owner's own inbox.
+ * @enum {boolean}
+ */
+ sandbox: true;
+ /**
+ * @description Delivery status at the moment of the response — `PENDING` for a send still queued.
+ * @enum {string}
+ */
+ status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /**
+ * Format: email
+ * @description The recipient the message was queued for.
+ */
+ to: string;
+ };
+ /** @description Receipt for a single transactional send. */
+ EmailV1: {
+ /**
+ * Format: email
+ * @description The sender actually used. Worth reading rather than assuming: it is resolved server-side and may come from the template when the request named none.
+ */
+ from: string;
+ /**
+ * Format: uuid
+ * @description The Email row this send created. Quote it in support requests.
+ */
+ id: string;
+ /**
+ * @description Delivery status at the moment of the response — `PENDING` for a send the worker has not picked up yet, which is the usual answer. Later states arrive via webhooks, not here.
+ * @enum {string}
+ */
+ status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /**
+ * Format: email
+ * @description The recipient the message was queued for.
+ */
+ to: string;
+ };
+ EmailValidationBatchRequestV1: {
+ /** @description The addresses to check, at most 50. Every distinct DOMAIN costs a DNS round trip, so this endpoint is bounded by latency rather than payload size — validate a whole list with `POST /api/v1/lists/{id}/validation-runs`, which is a background job. */
+ emails: string[];
+ };
+ /** @description One verdict per address, in the order they were given. */
+ EmailValidationBatchV1: {
+ results: components["schemas"]["EmailValidationV1"][];
+ };
+ /** @description One page of a run's results. No total — a run over a large list holds millions of rows, and the run's own counters are the numbers worth reading. */
+ EmailValidationResultListV1: {
+ data: (components["schemas"]["EmailValidationV1"] & {
+ contact_id: string | null;
+ })[];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description One bulk validation run over a list. */
+ EmailValidationRunV1: {
+ /** Format: date-time */
+ completed_at: string | null;
+ /** Format: date-time */
+ created_at: string;
+ deliverable_count: number;
+ /** @description Set only on `failed`. Prose for an operator; never parse it. */
+ failure_reason: string | null;
+ id: string;
+ list_id: string | null;
+ /** @description Addresses checked so far. There is deliberately no total: a list changes size while a run walks it, so a denominator captured up front would be wrong by the time you read it. */
+ processed_count: number;
+ risky_count: number;
+ /** Format: date-time */
+ started_at: string | null;
+ /** @enum {string} */
+ status: "pending" | "running" | "completed" | "failed";
+ undeliverable_count: number;
+ };
+ /** @description One address's verdict, with the evidence behind it. */
+ EmailValidationV1: {
+ email: string;
+ /** @description The domain publishes MX records. */
+ has_mx_records: boolean;
+ /** @description A throwaway-inbox provider. The ONLY flag here that lowers the verdict. */
+ is_disposable: boolean;
+ /** @description A free/consumer provider (Gmail, Outlook). List-quality information, not a problem. */
+ is_personal: boolean;
+ /** @description The local part addresses a role (`support@`, `info@`), not a person. List-quality information: role mailboxes are deliverable and companies answer them. */
+ is_role_address: boolean;
+ /** @description Human-readable findings. Prose for a person to read — branch on `verdict`, never on these. */
+ reasons: string[];
+ verdict: components["schemas"]["EmailValidationVerdictV1"];
+ };
+ /**
+ * @description `deliverable`: the domain resolves and accepts mail, with no badness signal. `undeliverable`: the domain does not exist or publishes no MX records. `risky`: deliverable, but a throwaway-inbox provider — mailing it costs sender reputation. `unknown`: DNS did not answer in time, so this address was NOT checked. Ask again; never delete a contact on `unknown`.
+ * @enum {string}
+ */
+ EmailValidationVerdictV1: "deliverable" | "undeliverable" | "risky" | "unknown";
+ /** @description A transactional email together with its delivery history. */
+ EmailWithEvents: components["schemas"]["Email"] & {
+ /** @description Delivery transitions for this message, oldest first. NOT the custom events recorded with `POST /api/v1/events` — those are a separate resource. */
+ events: components["schemas"]["EmailEvent"][];
+ };
+ /** @description Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`. */
+ Error: {
+ error: {
+ code: string;
details?: {
errors: unknown[];
};
- message: string;
+ message: string;
+ };
+ /** @enum {boolean} */
+ success?: false;
+ };
+ /** @description Every distinct event name in the project, most frequent first. */
+ EventNamesV1: {
+ data: string[];
+ };
+ /** @description Per-name event counts over the applied window. */
+ EventStatsV1: {
+ data: {
+ count: number;
+ name: string;
+ }[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description Body for POST /api/v1/events. */
+ EventTrackV1: {
+ /**
+ * Format: uuid
+ * @description Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event.
+ */
+ contact_id?: string;
+ /** @description Event name, e.g. `user.signup`. */
+ name: string;
+ /** @description Arbitrary event payload. */
+ payload?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ };
+ /** @description A recorded custom event. */
+ EventV1: {
+ /** Format: uuid */
+ contact_id: string | null;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: uuid */
+ email_id: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ /** @description The payload recorded with the event, or null. */
+ payload: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /** @description Cursor-paginated list of events, newest first. */
+ EventV1List: {
+ data: components["schemas"]["EventV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A filter condition: one or more groups combined with `logic`. */
+ FilterConditionV1: {
+ groups: components["schemas"]["FilterGroupV1"][];
+ /** @enum {string} */
+ logic: "AND" | "OR";
+ };
+ /** @description A group of filters. The filters inside one group ALWAYS combine with AND; `conditions` nests a further condition under this group, which is how OR-of-ANDs (and deeper) is expressed. */
+ FilterGroupV1: {
+ conditions?: components["schemas"]["FilterConditionV1"];
+ filters: components["schemas"]["SegmentFilterV1"][];
+ };
+ /** @description Success envelope carrying the affected resource's id, e.g. after a delete. */
+ IdResponse: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/lists/{id}/subscribe. */
+ ListSubscribe: {
+ /**
+ * @description Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add.
+ * @default false
+ */
+ allowResubscribe: boolean;
+ /** @description Custom fields to upsert onto the contact as part of subscribing. */
+ data?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ };
+ /** @description Result of a list-subscribe call. */
+ ListSubscribeResponse: {
+ data: {
+ /** @description Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm-subscription?token= to the contact. Valid for 24 hours. */
+ confirmToken?: string;
+ /** @description True when the membership row did not exist before this call. */
+ created: boolean;
+ /** Format: uuid */
+ membershipId: string;
+ /**
+ * @description Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user.
+ * @enum {string|null}
+ */
+ previousStatus: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED" | null;
+ /** @enum {string} */
+ status: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED";
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/lists/{id}/unsubscribe. */
+ ListUnsubscribe: {
+ /** Format: email */
+ email: string;
+ };
+ /** @description Echoes the address that was unsubscribed. */
+ ListUnsubscribeResponse: {
+ data: {
+ /** Format: email */
+ email: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A subscriber list as exposed on the v1 API. */
+ ListV1: {
+ /** Format: uuid */
+ confirmation_template_id: string | null;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ double_opt_in: boolean;
+ /** Format: uuid */
+ id: string;
+ /** @description Memberships in ANY status, including PENDING and UNSUBSCRIBED ones. */
+ member_count: number;
+ name: string;
+ redirect_url: string | null;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/lists. */
+ ListV1Create: {
+ /** Format: uuid */
+ confirmation_template_id?: string | null;
+ description?: string | null;
+ /**
+ * @description Require the contact to confirm before the membership becomes CONFIRMED. Sendly does NOT send the confirmation email — subscribing returns a `confirm_token` and you deliver `/api/lists/confirm-subscription?token=` to the contact yourself.
+ * @default false
+ */
+ double_opt_in: boolean;
+ name: string;
+ /**
+ * Format: uri
+ * @description Where a confirmed contact is sent after following the confirmation link.
+ */
+ redirect_url?: string | null;
+ };
+ /** @description Acknowledgement that a list was deleted. */
+ ListV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of subscriber lists. */
+ ListV1List: {
+ data: components["schemas"]["ListV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/lists/{id}. */
+ ListV1Update: {
+ /** Format: uuid */
+ confirmation_template_id?: string | null;
+ description?: string | null;
+ double_opt_in?: boolean;
+ name?: string;
+ /** Format: uri */
+ redirect_url?: string | null;
+ };
+ /** @description A receiving mailbox on one of the project's verified domains. */
+ Mailbox: {
+ /**
+ * Format: email
+ * @description The full mailbox address, e.g. `support@superbooks.io`.
+ */
+ address: string;
+ /** Format: date-time */
+ createdAt: string;
+ displayName: string | null;
+ /**
+ * Format: uuid
+ * @description The verified domain this mailbox lives on.
+ */
+ domainId: string;
+ /** Format: uuid */
+ id: string;
+ /** @description Always null. Mailbox quotas are not implemented — the value was never applied to the mail account — so this field reports the absence rather than a number nothing enforces. */
+ quotaBytes: number | null;
+ /**
+ * @description `PROVISIONING` while the mail account is being created, `ACTIVE` once it can receive, `SUSPENDED` when receiving is paused, `FAILED` when provisioning did not complete. A `FAILED` mailbox can be re-created with the same address — the retry reclaims the row.
+ * @enum {string}
+ */
+ status: "PROVISIONING" | "ACTIVE" | "SUSPENDED" | "FAILED";
+ };
+ /** @description A mailbox plus its IMAP/SMTP connection settings. */
+ MailboxDetail: components["schemas"]["Mailbox"] & {
+ /** @description Host, port and username for connecting a mail client. The PASSWORD is not here and is never returned by this endpoint — create an app password for that. */
+ settings: {
+ imap: {
+ host: string;
+ port: number;
+ /** @description Transport security, e.g. `SSL/TLS`. */
+ security: string;
+ /** @description The mailbox address — it is also the login. */
+ username: string;
+ };
+ smtp: {
+ host: string;
+ port: number;
+ /** @description Transport security, e.g. `SSL/TLS`. */
+ security: string;
+ /** @description The mailbox address — it is also the login. */
+ username: string;
+ };
+ };
+ };
+ /** @description RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface. */
+ Problem: {
+ /** @description Machine-readable lowercase error code, e.g. `scope_missing`. */
+ code: string;
+ /** @description Explanation specific to this occurrence. */
+ detail?: string;
+ /** @description Field-level failures. Present on 422 `validation_error` responses. */
+ errors?: {
+ code: string;
+ message: string;
+ /** @description RFC 6901 JSON Pointer to the offending field. */
+ pointer: string;
+ }[];
+ /** @description Request path the failure occurred on. */
+ instance?: string;
+ /** @description Correlation id — quote it in support requests. */
+ request_id?: string;
+ /** @description HTTP status code, repeated in the body. */
+ status: number;
+ /** @description Short, stable summary — the same for every occurrence of a `type`. */
+ title: string;
+ /**
+ * Format: uri
+ * @description Dereferenceable URI identifying the error class, anchored on the docs errors page.
+ */
+ type: string;
+ };
+ ProjectRecord: {
+ billingLimitCampaigns: number | null;
+ billingLimitInbound: number | null;
+ billingLimitTransactional: number | null;
+ billingLimitWorkflows: number | null;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ disabled: boolean;
+ disabledReason: string | null;
+ /** Format: uuid */
+ id: string;
+ /** @description ISO 639-1 code for customer-facing content. */
+ language: string;
+ name: string;
+ organizationId: string | null;
+ /** @description Local-part of the sandbox quick-start sender; null until first derived. */
+ sandboxHandle: string | null;
+ sesRegion: string | null;
+ stripeCustomerId: string | null;
+ stripeSubscriptionId: string | null;
+ /** @enum {string} */
+ tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description The project the presented credential is scoped to. */
+ ProjectV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description A disabled project sends nothing; every send is refused. */
+ disabled: boolean;
+ /** Format: uuid */
+ id: string;
+ /** @description ISO 639-1 code for customer-facing content. */
+ language: string;
+ name: string;
+ /** @description This project's quick-start sender, usable with no domain setup — but only to the project owner's own verified address, and under a daily cap. Null when none can be derived. */
+ sandbox_address: string | null;
+ /** @description Locked once the first domain is added. */
+ ses_region: string | null;
+ /** @enum {string} */
+ tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
+ };
+ /** @description Delivery outcomes for one recipient domain on one day. */
+ RecipientDomainStatsV1: {
+ bounced: number;
+ complained: number;
+ /**
+ * Format: date-time
+ * @description When the rollup job last rebuilt this row. These counts are a CACHE, refreshed hourly.
+ */
+ computed_at: string;
+ /** @description The UTC day these counts cover, as `YYYY-MM-DD`. */
+ day: string;
+ delivered: number;
+ /** @description The recipient's domain, lowercased: the part after the `@`. */
+ domain: string;
+ opened: number;
+ sent: number;
+ };
+ /** @description Cursor-paginated recipient-domain rollup, newest day first. */
+ RecipientDomainStatsV1List: {
+ data: components["schemas"]["RecipientDomainStatsV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A contact belonging to a segment. */
+ SegmentContactV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields: {
+ [key: string]: unknown;
+ };
+ email: string;
+ /** Format: uuid */
+ id: string;
+ subscribed: boolean;
+ };
+ /** @description Cursor-paginated list of the contacts belonging to a segment. */
+ SegmentContactV1List: {
+ data: components["schemas"]["SegmentContactV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description One comparison. `field` addresses a contact column (`email`, `createdAt`, …) or a `customFields.` path. `value` is whatever the operator compares against — its JSON type follows the field, and it is omitted entirely for the presence operators (`exists`, `notExists`). `unit` applies only to the relative-time operators (`within`, `olderThan`, and the `triggered*` family). */
+ SegmentFilterV1: {
+ field: string;
+ /** @enum {string} */
+ operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists" | "within" | "olderThan" | "triggered" | "triggeredWithin" | "triggeredOlderThan" | "notTriggered" | "notTriggeredWithin" | "isMemberOf";
+ /** @enum {string} */
+ unit?: "days" | "hours" | "minutes";
+ value?: unknown;
+ };
+ /** @description A segment as exposed on the v1 API. */
+ SegmentV1: {
+ condition: components["schemas"]["FilterConditionV1"] | null;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ /** Format: uuid */
+ id: string;
+ member_count: number;
+ name: string;
+ track_membership: boolean;
+ /** @enum {string} */
+ type: "DYNAMIC" | "STATIC";
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`. */
+ SegmentV1Create: {
+ condition?: components["schemas"]["FilterConditionV1"];
+ description?: string;
+ name: string;
+ /**
+ * @description Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition.
+ * @default false
+ */
+ track_membership: boolean;
+ /**
+ * @default DYNAMIC
+ * @enum {string}
+ */
+ type: "DYNAMIC" | "STATIC";
+ };
+ /** @description Acknowledgement that a segment was deleted. */
+ SegmentV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of segments. */
+ SegmentV1List: {
+ data: components["schemas"]["SegmentV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment. */
+ SegmentV1Update: {
+ condition?: components["schemas"]["FilterConditionV1"];
+ description?: string;
+ name?: string;
+ track_membership?: boolean;
+ };
+ /** @description Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required. */
+ SendEmail: {
+ attachments?: {
+ content: string;
+ contentId?: string;
+ contentType: string;
+ /**
+ * @default attachment
+ * @enum {string}
+ */
+ disposition: "attachment" | "inline";
+ filename: string;
+ }[];
+ bcc?: string[];
+ body?: string;
+ cc?: string[];
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ from?: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ headers?: {
+ [key: string]: string;
+ };
+ name?: string;
+ /** Format: email */
+ reply?: string;
+ subject?: string;
+ subscribed?: boolean;
+ tags?: string[];
+ /** Format: uuid */
+ template?: string;
+ to: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ } | (string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ })[];
+ };
+ /** @description Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`. */
+ SendEmailData: {
+ emails: components["schemas"]["SendEmailRecipientResult"][];
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @description Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient. */
+ SendEmailRecipientResult: {
+ contact: {
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ };
+ /** Format: uuid */
+ email: string;
+ };
+ /** @description Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status. */
+ SendEmailResponse: {
+ data: components["schemas"]["SendEmailData"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/v1/emails. Either `template` or `subject`+`body` is required, and `to` names exactly one recipient. */
+ SendEmailV1: {
+ attachments?: {
+ content: string;
+ contentId?: string;
+ contentType: string;
+ /**
+ * @default attachment
+ * @enum {string}
+ */
+ disposition: "attachment" | "inline";
+ filename: string;
+ }[];
+ bcc?: string[];
+ body?: string;
+ cc?: string[];
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ from?: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ headers?: {
+ [key: string]: string;
+ };
+ name?: string;
+ /** Format: email */
+ reply?: string;
+ subject?: string;
+ subscribed?: boolean;
+ tags?: string[];
+ /** Format: uuid */
+ template?: string;
+ /** @description The single recipient. Use `cc`/`bcc` to copy others on the same message. */
+ to: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ };
+ /** @description Body for POST /api/v1/emails/test. `subject` and `body` are required; `to` defaults to the project owner's verified email, and `from` is refused. */
+ SendTestEmailV1: {
+ /** @description HTML body. Merge tags are rendered as on any other send. */
+ body: string;
+ /** @description NOT ACCEPTED. The sender is always this project's sandbox address, resolved server-side; naming one here is refused rather than ignored, so a request that expects a different sender never gets a success it would misread. Read `sandbox_address` from `GET /api/v1/projects` to learn the address, or `from` off this response. */
+ from?: string;
+ subject: string;
+ /**
+ * Format: email
+ * @description Where to send it. Optional — it defaults to the project owner's own verified account email, which is the only address a sandbox send may reach. Any other value is refused.
+ */
+ to?: string;
+ };
+ /**
+ * @description Which traffic this identity carries. Omit to leave it unassigned, which lets it carry every stream — that is what every domain added before per-stream identities does.
+ * @enum {string}
+ */
+ SendingStream: "TRANSACTIONAL" | "MARKETING";
+ /** @description A reusable fragment of template markup. */
+ Snippet: {
+ /** @description Template markup. Values it interpolates are escaped like any other. */
+ body: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ description?: string | null;
+ /** Format: uuid */
+ id: string;
+ /** @description The literal identifier a template includes with `{{> name}}`. */
+ name: string;
+ /** Format: uuid */
+ projectId: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Cursor-paginated list of snippets. */
+ SnippetListResponse: {
+ data: {
+ /** @description Cursor for the next page; omitted on the last page. */
+ cursor?: string;
+ data: components["schemas"]["Snippet"][];
+ hasMore: boolean;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Bare success envelope with no payload. */
+ SuccessEmpty: {
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A single suppressed-email record. */
+ Suppression: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ /** Format: uuid */
+ projectId: string;
+ /** @enum {string} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /**
+ * @description How far the suppression reaches. `PROJECT` is every record this API creates or returns today.
+ * @enum {string}
+ */
+ scope: "PROJECT" | "GLOBAL";
+ /** @enum {string} */
+ source: "SES_WEBHOOK" | "API" | "DASHBOARD";
+ };
+ /** @description Result of GET /api/suppression/{email} — whether the address is suppressed. */
+ SuppressionCheckResponse: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt?: string;
+ /** @enum {string} */
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /** @enum {string} */
+ source?: "SES_WEBHOOK" | "API" | "DASHBOARD";
+ suppressed: boolean;
+ };
+ /** @description Cursor-paginated list of suppressions. NOTE: this route answers a bare body — there is no `{success, data}` envelope. */
+ SuppressionListResponse: {
+ items: components["schemas"]["Suppression"][];
+ /** @description Cursor for the next page, or `null` on the last page. Never omitted. */
+ nextCursor: string | null;
+ };
+ /** @description A suppressed address as exposed on the v1 API. */
+ SuppressionV1: {
+ /** Format: date-time */
+ created_at: string;
+ email: string;
+ /** @enum {string} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ source: string;
+ };
+ /** @description Body for POST /api/v1/suppressions. */
+ SuppressionV1Create: {
+ /** Format: email */
+ email: string;
+ /**
+ * @default MANUAL
+ * @enum {string}
+ */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
+ /** @description Acknowledgement that an address was un-suppressed. */
+ SuppressionV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ email: string;
+ };
+ /** @description Cursor-paginated list of suppressed addresses. */
+ SuppressionV1List: {
+ data: components["schemas"]["SuppressionV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A reusable email template. */
+ Template: {
+ body: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** @description Version counter, incremented by an update that changes the rendered content. A campaign records the version it sent, so this is how a caller tells 'the template changed since' from 'the template was renamed'. */
+ currentVersion: number;
+ description?: string | null;
+ /** @enum {string} */
+ emailCategory: "MARKETING" | "TRANSACTIONAL" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from: string;
+ fromName?: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ /** Format: uuid */
+ projectId: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Cursor-paginated list of templates. */
+ TemplateListResponse: {
+ data: {
+ /** @description Cursor for the next page; omitted on the last page. */
+ cursor?: string;
+ data: components["schemas"]["Template"][];
+ hasMore: boolean;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description An email template as exposed on the v1 API. */
+ TemplateV1: {
+ body: string;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ /** @enum {string} */
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ from: string;
+ from_name: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ reply_to: string | null;
+ subject: string;
+ /** Format: date-time */
+ updated_at: string;
+ version: number;
+ };
+ /** @description Body for POST /api/v1/templates. */
+ TemplateV1Create: {
+ body: string;
+ description?: string | null;
+ /**
+ * @default MARKETING
+ * @enum {string}
+ */
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /**
+ * Format: email
+ * @description Sender address. Its domain must be verified for this project.
+ */
+ from: string;
+ from_name?: string | null;
+ name: string;
+ /** Format: email */
+ reply_to?: string | null;
+ subject: string;
+ };
+ /** @description Acknowledgement that a template was deleted. */
+ TemplateV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of templates. */
+ TemplateV1List: {
+ data: components["schemas"]["TemplateV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/templates/{id}. */
+ TemplateV1Update: {
+ body?: string;
+ description?: string | null;
+ /** @enum {string} */
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from?: string;
+ from_name?: string | null;
+ name?: string;
+ /** Format: email */
+ reply_to?: string | null;
+ subject?: string;
+ };
+ TopicCreateV1: {
+ default_opt_in?: boolean;
+ description?: string | null;
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ };
+ /** @description One page of the subjects this project mails about. */
+ TopicListV1: {
+ data: components["schemas"]["TopicV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ TopicSubscribeV1: {
+ /** Format: uuid */
+ contact_id: string;
+ /** @description True asks to subscribe, which starts a DOUBLE OPT-IN: the contact is parked at `pending` and a confirmation link is sent. There is no way to skip that from the API — a subscription an API caller asserts is not evidence the mailbox holder agreed, and treating it as consent is exactly what double opt-in exists to stop. False records an unsubscribe, which takes effect immediately. */
+ subscribed: boolean;
+ };
+ /**
+ * @description `subscribed`: mail them about this. `unsubscribed`: do not. `pending`: a confirmation link was sent and has NOT been clicked — never treat this as consent, which is the whole point of double opt-in.
+ * @enum {string}
+ */
+ TopicSubscriptionStatusV1: "pending" | "subscribed" | "unsubscribed";
+ TopicSubscriptionV1: {
+ /** @description Present only when this call started a double opt-in. Sendly does NOT send the confirmation email — you do, from your own verified domain, because it is your relationship with the contact and your sending reputation. The subscription stays `pending`, and is NOT mailed, until someone opens this link. */
+ confirmation_url: string | null;
+ /** Format: date-time */
+ confirmed_at: string | null;
+ contact_id: string;
+ status: components["schemas"]["TopicSubscriptionStatusV1"];
+ topic_id: string;
+ };
+ /** @description `key` is deliberately absent. It is the name every stored preference and every integration refers to, so changing it would silently orphan them. */
+ TopicUpdateV1: {
+ archived?: boolean;
+ default_opt_in?: boolean;
+ description?: string | null;
+ name?: string;
+ };
+ /** @description One subject this project mails about. */
+ TopicV1: {
+ /** @description Hidden from the preference centre and from new sends, WITHOUT discarding the opt-outs recorded against it. There is no delete for the same reason: deleting a topic would delete the choices people made about it. */
+ archived: boolean;
+ /** Format: date-time */
+ created_at: string;
+ /** @description What a contact with NO answer counts as. True is the honest default for a topic added after a list already exists: those contacts consented to hear from you, and inventing an opt-out they never asked for would silence mail they expect. False means the topic must be opted INTO, and absence means `not asked` rather than `no`. */
+ default_opt_in: boolean;
+ description: string | null;
+ id: string;
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ /** @description Contacts who explicitly said yes. Excludes those covered only by `default_opt_in`. */
+ subscribed_count: number;
+ unsubscribed_count: number;
+ };
+ /** @description Body for POST /api/track — record a custom event for a contact. */
+ TrackEvent: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ event: string;
+ subscribed?: boolean;
+ };
+ /** @description Response from POST /api/track. */
+ TrackEventResponse: {
+ data: {
+ /** Format: uuid */
+ contact: string;
+ /** Format: uuid */
+ event: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses. */
+ UpdateContactBody: {
+ customFields?: {
+ [key: string]: unknown;
+ };
+ subscribed?: boolean;
+ };
+ /** @description Body for PATCH /api/snippets/{id}. */
+ UpdateSnippet: {
+ body?: string;
+ description?: string | null;
+ name?: string;
+ };
+ /** @description Body for PATCH /api/templates/{id}. */
+ UpdateTemplate: {
+ body?: string;
+ description?: string;
+ /** @enum {string} */
+ emailCategory?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from?: string;
+ fromName?: string | null;
+ name?: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject?: string;
+ };
+ /** @description Body for PATCH /api/webhooks/{id}. */
+ UpdateWebhook: {
+ eventTypes?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** @enum {string} */
+ status?: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: uri */
+ url?: string;
+ };
+ /** @description Current email usage against the limits that are actually enforced. */
+ UsageV1: {
+ daily: {
+ /** @description Today's sends. Null when the counter could not be read. */
+ emails_sent: number | null;
+ limit: number;
+ /** @enum {string} */
+ trust_tier: "NEW" | "ESTABLISHED" | "TRUSTED";
+ };
+ monthly: {
+ categories: {
+ campaign: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ inbound: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ transactional: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ workflow: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ };
+ emails_sent: number;
+ /** @description Monthly cap on the total. Null when per-category limits govern instead. */
+ limit: number | null;
+ };
+ /**
+ * @description `custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`.
+ * @enum {string}
+ */
+ plan: "free" | "pro" | "custom";
+ };
+ /** @description Body for POST /api/verify — validate email syntax, MX, disposable, etc. */
+ VerifyEmail: {
+ /** Format: email */
+ email: string;
+ };
+ /** @description Response from POST /api/verify — outcome of the syntax/MX/disposable check. */
+ VerifyEmailResponse: {
+ data: {
+ /** Format: email */
+ email: string;
+ reason?: string;
+ valid: boolean;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A user-managed outbound webhook. Never carries a secret. */
+ Webhook: {
+ consecutiveFailures: number;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ disabledAt?: string | null;
+ /** @description Sending domains this endpoint is scoped to. Empty means every domain on the project. */
+ domains: string[];
+ eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description While a rotation is in flight, when the OLD secret stops being accepted. `null` outside a rotation.
+ */
+ previousSecretExpiresAt?: string | null;
+ /** Format: uuid */
+ projectId: string;
+ /** @enum {string} */
+ status: "ACTIVE" | "PAUSED" | "DISABLED";
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ /** Format: uri */
+ url: string;
+ };
+ /** @description An attempted webhook delivery. */
+ WebhookCall: {
+ attempt: number;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ eventType: string;
+ /** Format: uuid */
+ id: string;
+ payload: {
+ [key: string]: unknown;
+ };
+ responseBody?: string | null;
+ responseStatus?: number | null;
+ /** @enum {string} */
+ status: "PENDING" | "SUCCESS" | "FAILED";
+ /** Format: uuid */
+ webhookId: string;
+ };
+ /** @description Cursor-paginated list of recent calls for a single webhook. */
+ WebhookCallsListResponse: {
+ cursor?: string | null;
+ data: components["schemas"]["WebhookCall"][];
+ hasMore?: boolean;
+ nextCursor?: string | null;
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely. */
+ WebhookCreateResponse: {
+ data: {
+ /** @description Plaintext shared secret. Returned ONCE on create. */
+ secret: string;
+ webhook: components["schemas"]["Webhook"];
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Single webhook (no secret). */
+ WebhookGetResponse: {
+ data: components["schemas"]["Webhook"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description List of webhooks for the auth'd project. */
+ WebhookListResponse: {
+ data: components["schemas"]["Webhook"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once. */
+ WebhookRotateSecretResponse: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ /** @description New plaintext shared secret. */
+ secret: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A webhook endpoint as exposed on the v1 API. The signing secret is NEVER on this shape — it is returned once, by create and by rotate, and no endpoint reads it back. */
+ WebhookV1: {
+ /** Format: date-time */
+ created_at: string;
+ event_types: string[];
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ status: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: date-time */
+ updated_at: string;
+ url: string;
+ };
+ /** @description Body for POST /api/v1/webhooks. */
+ WebhookV1Create: {
+ event_types: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uri */
+ url: string;
+ };
+ /** @description A newly created webhook and its one-time signing secret. */
+ WebhookV1Created: {
+ /** @description The signing secret, shown EXACTLY ONCE. Store it now — no endpoint returns it again. */
+ secret: string;
+ webhook: components["schemas"]["WebhookV1"];
+ };
+ /** @description Acknowledgement that a webhook was deleted. */
+ WebhookV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of webhook endpoints. */
+ WebhookV1List: {
+ data: components["schemas"]["WebhookV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A freshly rotated signing secret, and the moment the outgoing one stops verifying. */
+ WebhookV1SecretRotated: {
+ /**
+ * Format: date-time
+ * @description When the PREVIOUS secret stops verifying. Until then every delivery carries both signatures, so a consumer can redeploy its verifier without dropping an event.
+ */
+ previous_secret_expires_at: string;
+ /** @description The new signing secret, shown EXACTLY ONCE. Store it now — no endpoint returns it again. */
+ secret: string;
+ };
+ /** @description Body for PATCH /api/v1/webhooks/{id}. */
+ WebhookV1Update: {
+ event_types?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** @enum {string} */
+ status?: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: uri */
+ url?: string;
+ };
+ /** @description Body for `POST /api/v1/workflows/{id}/clone`. */
+ WorkflowCloneV1: {
+ /** @description Name for the copy. Defaults to `Copy of `. */
+ name?: string;
+ };
+ /** @description Branches the run. Binary form: `field` + `operator` + `value`, whose outgoing transitions carry `{ "branch": "yes" }` / `{ "branch": "no" }`. Multi form: `mode: "multi"` + `field` + `branches`, whose transitions carry the branch id. */
+ WorkflowConditionStepV1: {
+ config: {
+ branches?: ({
+ id: string;
+ name: string;
+ /** @enum {string} */
+ operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists";
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ value?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ })[];
+ field?: string;
+ /** @enum {string} */
+ mode?: "multi";
+ /** @enum {string} */
+ operator?: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists";
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ value?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "CONDITION";
+ };
+ /** @description Body for POST /api/v1/workflows. */
+ WorkflowCreateV1: {
+ allow_reentry?: boolean;
+ description?: string;
+ /** @description Workflows are created disabled. A workflow can only be enabled once every step is configured. */
+ enabled?: boolean;
+ /** @description The custom event that starts this workflow, e.g. `user.signup`. Required for `EVENT` workflows (the default) and ignored for the other trigger types. */
+ event_name?: string;
+ /** @description For `SCHEDULE` workflows: how often the workflow fires, in milliseconds. Between one minute and 30 days; defaults to one hour. */
+ interval_ms?: number;
+ name: string;
+ /** @description Optional LINEAR chain to create the workflow with, in run order. The trigger step is prepended and each step is wired to the next, so this cannot express branches — use `PUT /api/v1/workflows/{id}/graph` for those. Omit it to create a workflow that holds only its trigger step. */
+ sequence?: components["schemas"]["WorkflowSequenceStepV1"][];
+ trigger_type?: components["schemas"]["WorkflowTriggerTypeV1"];
+ };
+ /** @description Pauses the run for `amount` × `unit`, up to 365 days. */
+ WorkflowDelayStepV1: {
+ config: {
+ amount?: number;
+ /** @enum {string} */
+ unit?: "minutes" | "hours" | "days";
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "DELAY";
+ };
+ /** @description Confirmation that a workflow was deleted. */
+ WorkflowDeletedV1: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Body for POST /api/v1/workflows/{id}/executions. */
+ WorkflowExecutionStartV1: {
+ /**
+ * Format: uuid
+ * @description Contact to enter the workflow. Must belong to this project.
+ */
+ contact_id: string;
+ /** @description Extra variables merged into the contact's data for this run. */
+ context?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ };
+ /** @description One contact's run through a workflow. */
+ WorkflowExecutionV1: {
+ /** Format: date-time */
+ completed_at: string | null;
+ /** Format: uuid */
+ contact_id: string;
+ /** Format: uuid */
+ current_step_id: string | null;
+ exit_reason: string | null;
+ /** Format: uuid */
+ id: string;
+ /** Format: date-time */
+ started_at: string;
+ /** @enum {string} */
+ status: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Cursor-paginated list of workflow executions, newest first. */
+ WorkflowExecutionV1List: {
+ data: components["schemas"]["WorkflowExecutionV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Ends the run early and stamps `exit_reason`. */
+ WorkflowExitStepV1: {
+ config: {
+ reason?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "EXIT";
+ };
+ /** @description Body for `PUT /api/v1/workflows/{id}/graph`. Replaces the whole graph: a step whose id you send is kept and updated, a fresh id is created, and a step you omit is deleted along with its run history. Refused with 409 while the workflow has running executions, because those runs are standing on the steps being replaced. */
+ WorkflowGraphReplaceV1: {
+ /** @description The complete step set. Exactly one must be a `TRIGGER`. */
+ steps: components["schemas"]["WorkflowStepV1"][];
+ /** @description The complete edge set. Every `from_step_id`/`to_step_id` must name a step in this same document, and a step may not point at itself. */
+ transitions: components["schemas"]["WorkflowTransitionV1"][];
+ };
+ /** @description A workflow's complete step graph. The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path. */
+ WorkflowGraphV1: {
+ steps: components["schemas"]["WorkflowStepReadV1"][];
+ transitions: components["schemas"]["WorkflowTransitionV1"][];
+ /** @description The workflow's version AFTER this read. Every structural change bumps it and writes a `workflow_versions` snapshot, so a changed number between two reads means somebody edited the graph. */
+ version: number;
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Like `SEND_EMAIL`, but held until this contact's historically best open hour, falling back to `fallbackHour` and never waiting longer than `maxDelayHours`. */
+ WorkflowSendAtOptimalTimeStepV1: {
+ config: {
+ fallbackHour?: number;
+ maxDelayHours?: number;
+ /** Format: uuid */
+ templateId?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "SEND_AT_OPTIMAL_TIME";
+ };
+ /** @description Sends one email to the contact. Give it either `template_id` (preferred) or an inline `subject` + `body`. */
+ WorkflowSendEmailStepV1: {
+ config: {
+ body?: string;
+ recipient?: {
+ /** Format: email */
+ customEmail?: string;
+ /** @enum {string} */
+ type: "CONTACT" | "CUSTOM";
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ subject?: string;
+ /** Format: uuid */
+ templateId?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "SEND_EMAIL";
+ };
+ /**
+ * @description A step kind that may appear in a linear `sequence`. `TRIGGER` is prepended by the server.
+ * @enum {string}
+ */
+ WorkflowSequenceStepTypeV1: "SEND_EMAIL" | "DELAY" | "WAIT_FOR_EVENT" | "CONDITION" | "EXIT" | "WEBHOOK" | "UPDATE_CONTACT" | "SEND_AT_OPTIMAL_TIME";
+ /** @description One step of a linear workflow sequence. */
+ WorkflowSequenceStepV1: {
+ /** @description Step configuration. Keys are camelCase — see `WorkflowStepV1` for the shape each step type expects. */
+ config: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /** @description Human-readable label, e.g. `Day 0: welcome`. */
+ name: string;
+ /**
+ * Format: uuid
+ * @description For `SEND_EMAIL`: a template in this project.
+ */
+ template_id?: string;
+ type: components["schemas"]["WorkflowSequenceStepTypeV1"];
+ };
+ /** @description The workflow after a pause or resume, with the number of runs the call stopped. */
+ WorkflowStateChangeV1: {
+ /** @description Runs stopped by this call. Always 0 for `resume`; on `pause` it is the number of `RUNNING`/`WAITING` executions that were cancelled, which is what makes pausing different from `PATCH { enabled: false }` (that only stops NEW runs starting). */
+ cancelled_executions: number;
+ workflow: components["schemas"]["WorkflowV1"];
+ };
+ /** @description Execution, email and conversion totals for one workflow. */
+ WorkflowStatsV1: {
+ avg_duration_ms: number | null;
+ /** @description Execution counts keyed by status; a status with no executions is absent. */
+ by_status: {
+ [key: string]: number;
+ };
+ /** @description Completed ÷ finished executions (0–1). Null until at least one execution has finished. */
+ completion_rate: number | null;
+ conversions: {
+ count: number;
+ event_name: string;
+ /** Format: uuid */
+ goal_id: string;
+ name: string;
+ }[];
+ emails: {
+ clicked: number;
+ opened: number;
+ sent: number;
+ };
+ enabled: boolean;
+ name: string;
+ /** @description Steps in the workflow's graph, trigger step included. */
+ step_count: number;
+ total: number;
+ trigger_type: components["schemas"]["WorkflowTriggerTypeV1"] & unknown;
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Where this step sits on the editor canvas. */
+ WorkflowStepPositionV1: {
+ x: number;
+ y: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /** @description One node of a workflow graph, as read. */
+ WorkflowStepReadV1: {
+ /** @description The step's configuration, exactly as stored. See `WorkflowStepV1` for the keys each step type uses; keys are camelCase. */
+ config: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /** @enum {string} */
+ type: "TRIGGER" | "SEND_EMAIL" | "DELAY" | "WAIT_FOR_EVENT" | "CONDITION" | "EXIT" | "WEBHOOK" | "UPDATE_CONTACT" | "SEND_AT_OPTIMAL_TIME";
+ };
+ /** @description One node of a workflow graph. */
+ WorkflowStepV1: components["schemas"]["WorkflowTriggerStepV1"] | components["schemas"]["WorkflowSendEmailStepV1"] | components["schemas"]["WorkflowDelayStepV1"] | components["schemas"]["WorkflowWaitForEventStepV1"] | components["schemas"]["WorkflowConditionStepV1"] | components["schemas"]["WorkflowExitStepV1"] | components["schemas"]["WorkflowWebhookStepV1"] | components["schemas"]["WorkflowUpdateContactStepV1"] | components["schemas"]["WorkflowSendAtOptimalTimeStepV1"];
+ /** @description One directed edge between two steps. */
+ WorkflowTransitionV1: {
+ /** @description Null to always follow this edge. From a `CONDITION` step, `{ "branch": "yes" }`, `{ "branch": "no" }`, or `{ "branch": "" }` in the multi form. */
+ condition: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ /** Format: uuid */
+ from_step_id: string;
+ /**
+ * Format: uuid
+ * @description Caller-chosen on a write, exactly like a step id.
+ */
+ id: string;
+ /** @description Evaluation order among the edges leaving one step; lowest first. */
+ priority: number;
+ /** Format: uuid */
+ to_step_id: string;
+ };
+ /** @description The graph's single entry node. Its config mirrors the workflow's own trigger: `eventName` for `EVENT`, `intervalMs` for `SCHEDULE`, empty for `MANUAL`. */
+ WorkflowTriggerStepV1: {
+ config: {
+ eventName?: string;
+ intervalMs?: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "TRIGGER";
+ };
+ /**
+ * @description What starts this workflow. Defaults to `EVENT`, which is the only kind the API created before this field existed. `EVENT` requires `event_name`; `SCHEDULE` uses `interval_ms`; `MANUAL` is started only by `POST /api/v1/workflows/{id}/executions`.
+ * @enum {string}
+ */
+ WorkflowTriggerTypeV1: "EVENT" | "MANUAL" | "SCHEDULE";
+ /** @description Writes `updates` onto the contact, and optionally flips `subscribed`. */
+ WorkflowUpdateContactStepV1: {
+ config: {
+ subscribed?: boolean;
+ updates?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "UPDATE_CONTACT";
+ };
+ /** @description Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409. */
+ WorkflowUpdateV1: {
+ allow_reentry?: boolean;
+ description?: string;
+ enabled?: boolean;
+ event_name?: string;
+ /** @description For `SCHEDULE` workflows: how often the workflow fires, in milliseconds. Between one minute and 30 days; defaults to one hour. */
+ interval_ms?: number;
+ /** @description Per-workflow start rate cap. `null` removes the cap. */
+ max_executions_per_hour?: number | null;
+ name?: string;
+ /** @description Optional LINEAR chain that REPLACES every non-trigger step, in run order. Destructive: the existing steps are discarded and their ids do not survive, which is why omitting this field leaves the graph untouched. Branches need `PUT /api/v1/workflows/{id}/graph` instead. */
+ sequence?: components["schemas"]["WorkflowSequenceStepV1"][];
+ trigger_type?: components["schemas"]["WorkflowTriggerTypeV1"] & unknown;
+ };
+ /** @description An automation workflow as exposed on the v1 API. */
+ WorkflowV1: {
+ allow_reentry: boolean;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ enabled: boolean;
+ /** @description Trigger event for `EVENT` workflows; null for the other trigger types. */
+ event_name: string | null;
+ /** Format: uuid */
+ id: string;
+ max_executions_per_hour: number | null;
+ name: string;
+ /** @description Steps in this workflow's graph, trigger step included — so a workflow that has only ever been created reports 1. Read `/graph` for the steps themselves. */
+ step_count: number;
+ /** @enum {string} */
+ trigger_type: "EVENT" | "MANUAL" | "SCHEDULE";
+ /** Format: date-time */
+ updated_at: string;
+ /** @description Incremented on every structural (step/transition) change. */
+ version: number;
+ };
+ /** @description Cursor-paginated list of workflows. */
+ WorkflowV1List: {
+ data: components["schemas"]["WorkflowV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Parks the run until `eventName` is recorded for this contact, or `timeout` seconds pass. */
+ WorkflowWaitForEventStepV1: {
+ config: {
+ eventName?: string;
+ timeout?: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "WAIT_FOR_EVENT";
+ };
+ /** @description Calls an external URL. `url`, header values and the JSON body's string leaves are `{{variable}}`-interpolated from the contact and the run's variables. */
+ WorkflowWebhookStepV1: {
+ config: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ body?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
+ /** Format: uri */
+ url?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "WEBHOOK";
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+interface operations {
+ listContacts: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ subscribed?: "true" | "false";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ContactListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateContact"];
+ };
+ };
+ responses: {
+ /** @description Contact created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Email already exists for this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ bulkCreateContacts: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactBulkCreateBody"];
+ };
+ };
+ responses: {
+ /** @description Bulk-create result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ created: number;
+ errors: {
+ index: number;
+ message: string;
+ }[];
+ skipped: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ bulkDeleteContacts: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactBulkDeleteBody"];
+ };
+ };
+ responses: {
+ /** @description Bulk-delete result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ deleted: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ upsertContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateContact"];
+ };
+ };
+ responses: {
+ /** @description Contact created or updated */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ updateContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateContactBody"];
+ };
+ };
+ responses: {
+ /** @description Updated contact */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listDomains: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DomainListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ addDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AddDomainBody"];
+ };
+ };
+ responses: {
+ /** @description Domain added */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault — retrying it unchanged will not help. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain removed */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuccessEmpty"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ assignDomainStream: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AssignDomainStream"];
+ };
+ };
+ responses: {
+ /** @description Updated sending identity */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ startDomainSetup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Guided setup session */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /**
+ * Format: uri
+ * @description Open this in a browser to publish the records. Short-lived and domain-specific.
+ */
+ connectUrl: string;
+ /** @description When `connectUrl` stops working. */
+ expiresAt: string;
+ token: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDomainVerification: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Verification status */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["DomainVerificationStatus"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ verifyDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Verification status */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["DomainVerificationStatus"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listEmails: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ tag?: string;
+ /** @description Delivery lifecycle of the message. Engagement is reported separately. */
+ status?: components["schemas"]["EmailDeliveryStatus"];
+ from?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendEmail: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendEmail"];
+ };
+ };
+ responses: {
+ /** @description Email accepted / sent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SendEmailResponse"];
+ };
+ };
+ /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendEmailBatch: {
+ parameters: {
+ query?: never;
+ header?: {
+ "Idempotency-Key"?: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchSendBody"];
+ };
+ };
+ responses: {
+ /** @description All entries sent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchSendResponse"];
+ };
+ };
+ /** @description Partial success — at least one entry failed */
+ 207: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchSendResponse"];
+ };
+ };
+ /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email and its delivery history */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailDetailResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ cancelScheduledEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email cancelled */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Email already past PENDING */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ subscribeToList: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description List id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListSubscribe"];
+ };
+ };
+ responses: {
+ /** @description Contact subscribed, or an existing membership returned unchanged */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ListSubscribeResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ unsubscribeFromList: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description List id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListUnsubscribe"];
+ };
+ };
+ responses: {
+ /** @description Contact unsubscribed */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ListUnsubscribeResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listMailboxes: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Mailbox"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateMailboxBody"];
+ };
+ };
+ responses: {
+ /** @description Mailbox provisioned */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Mailbox"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The address already exists, the domain is not verified, or the project is at its 10-mailbox limit. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Provisioning failed in the mail server. The mailbox row is left `FAILED` and can be retried. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox with connection settings */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["MailboxDetail"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @enum {boolean} */
+ deleted: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listAppPasswords: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description App password list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["AppPassword"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createAppPassword: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateAppPassword"];
+ };
+ };
+ responses: {
+ /** @description App password created; the secret is behind the one-time link */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["AppPasswordReveal"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ revokeAppPassword: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ passwordId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description App password revoked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @enum {boolean} */
+ revoked: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ draftMailboxMessage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["DraftMailboxMessage"];
+ };
+ };
+ responses: {
+ /** @description A draft. Nothing was sent. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @description Suggested plain-text body, or null. */
+ body: string | null;
+ /**
+ * @description Always false. Reported rather than assumed, so a draft cannot be mistaken for a send.
+ * @enum {boolean}
+ */
+ sent: false;
+ /** @description Suggested subject, or null. */
+ subject: string | null;
+ /** @description Alternative subject lines (`subject` mode); empty otherwise. */
+ subjects: string[];
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The drafting model was unreachable or returned nothing usable. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendMailboxMessage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ComposeMailboxMessage"];
+ };
+ };
+ responses: {
+ /** @description Message submitted */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /**
+ * Format: uuid
+ * @description The conversation this send started. Replies thread onto it.
+ */
+ conversationId: string;
+ /**
+ * Format: uuid
+ * @description The stored outbound message.
+ */
+ messageId: string;
+ /** @enum {boolean} */
+ submitted: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The mailbox is not active, a recipient is suppressed (`RECIPIENT_SUPPRESSED`), or the content scanner refused the message (`CONTENT_REFUSED`). */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The mail server refused the submission. Nothing was sent. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Message screening could not reach a verdict. Nothing was sent; retry shortly. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listApiKeys: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ApiKeyListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateApiKeyBody"];
+ };
+ };
+ responses: {
+ /** @description API key created; the secret is behind the reveal link. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
+ data: components["schemas"]["ApiKey"] & {
+ /**
+ * Format: date-time
+ * @description When the reveal link stops working. Create or rotate again to get a new one.
+ */
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
+ */
+ revealUrl: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ revokeApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ /** @description API key id. */
+ keyId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key revoked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuccessEmpty"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ rotateApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ /** @description API key id. */
+ keyId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key rotated; the new secret is behind the reveal link. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ lastFour: string;
+ /**
+ * Format: date-time
+ * @description When the reveal link stops working. Create or rotate again to get a new one.
+ */
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
+ */
+ revealUrl: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listSnippets: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SnippetListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateSnippet"];
+ };
+ };
+ responses: {
+ /** @description Snippet created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description A snippet with that name already exists in this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ updateSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSnippet"];
+ };
+ };
+ responses: {
+ /** @description Updated snippet */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description A snippet with that name already exists in this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listSuppressions: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuppressionListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ addSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AddSuppression"];
+ };
+ };
+ responses: {
+ /** @description Suppression added */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Suppression"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ checkSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description URL-encoded email address */
+ email: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression check result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuppressionCheckResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ removeSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description URL-encoded email address */
+ email: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression removed */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listTemplates: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ emailCategory?: "MARKETING" | "TRANSACTIONAL" | "SELF_MANAGED_UNSUBSCRIBE";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Template list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TemplateListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateTemplate"];
+ };
+ };
+ responses: {
+ /** @description Template created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** @enum {boolean} */
- success?: false;
- };
- /** @description Every distinct event name in the project, most frequent first. */
- EventNamesV1: {
- data: string[];
- };
- /** @description Per-name event counts over the applied window. */
- EventStatsV1: {
- data: {
- count: number;
- name: string;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
- };
- /** @description Body for POST /api/v1/events. */
- EventTrackV1: {
- /**
- * Format: uuid
- * @description Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event.
- */
- contact_id?: string;
- /** @description Arbitrary event payload. */
- data?: {
- [key: string]: string | number | boolean | {
- [key: string]: unknown;
- } | unknown[] | null;
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** @description Event name, e.g. `user.signup`. */
- name: string;
- };
- /** @description A recorded custom event. */
- EventV1: {
- /** Format: uuid */
- contact_id: string | null;
- /** Format: date-time */
- created_at: string;
- /** @description The payload recorded with the event, or null. */
- data: {
- [key: string]: unknown;
- } | null;
- /** Format: uuid */
- email_id: string | null;
- /** Format: uuid */
- id: string;
- name: string;
- };
- /** @description Cursor-paginated list of events, newest first. */
- EventV1List: {
- data: components["schemas"]["EventV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
};
- /** @description A filter condition: one or more groups combined with `logic`. */
- FilterConditionV1: {
- groups: components["schemas"]["FilterGroupV1"][];
- /** @enum {string} */
- logic: "AND" | "OR";
+ };
+ getTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
};
- /** @description A group of filters. The filters inside one group ALWAYS combine with AND; `conditions` nests a further condition under this group, which is how OR-of-ANDs (and deeper) is expressed. */
- FilterGroupV1: {
- conditions?: components["schemas"]["FilterConditionV1"];
- filters: components["schemas"]["SegmentFilterV1"][];
+ requestBody?: never;
+ responses: {
+ /** @description Template */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Success envelope carrying the affected resource's id, e.g. after a delete. */
- IdResponse: {
- data: {
- /** Format: uuid */
+ };
+ deleteTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
id: string;
};
- /** @enum {boolean} */
- success: true;
+ cookie?: never;
};
- /** @description Body for POST /api/lists/{id}/subscribe. */
- ListSubscribe: {
- /**
- * @description Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add.
- * @default false
- */
- allowResubscribe: boolean;
- /** @description Custom fields to upsert onto the contact as part of subscribing. */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Template deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Template still in use */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** Format: email */
- email: string;
};
- /** @description Result of a list-subscribe call. */
- ListSubscribeResponse: {
- data: {
- /** @description Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm?token= to the contact. Valid for 24 hours. */
- confirmToken?: string;
- /** @description True when the membership row did not exist before this call. */
- created: boolean;
- /** Format: uuid */
- membershipId: string;
- /**
- * @description Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user.
- * @enum {string|null}
- */
- previousStatus: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED" | null;
- /** @enum {string} */
- status: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED";
+ };
+ updateTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/lists/{id}/unsubscribe. */
- ListUnsubscribe: {
- /** Format: email */
- email: string;
+ cookie?: never;
};
- /** @description Echoes the address that was unsubscribed. */
- ListUnsubscribeResponse: {
- data: {
- /** Format: email */
- email: string;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateTemplate"];
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description A receiving mailbox on one of the project's verified domains. */
- Mailbox: {
- /**
- * Format: email
- * @description The full mailbox address, e.g. `support@superbooks.io`.
- */
- address: string;
- /** Format: date-time */
- createdAt: string;
- displayName: string | null;
- /**
- * Format: uuid
- * @description The verified domain this mailbox lives on.
- */
- domainId: string;
- /** Format: uuid */
- id: string;
- /** @description Always null. Mailbox quotas are not implemented — the value was never applied to the mail account — so this field reports the absence rather than a number nothing enforces. */
- quotaBytes: number | null;
- /**
- * @description `PROVISIONING` while the mail account is being created, `ACTIVE` once it can receive, `SUSPENDED` when receiving is paused, `FAILED` when provisioning did not complete. A `FAILED` mailbox can be re-created with the same address — the retry reclaims the row.
- * @enum {string}
- */
- status: "PROVISIONING" | "ACTIVE" | "SUSPENDED" | "FAILED";
};
- /** @description A mailbox plus its IMAP/SMTP connection settings. */
- MailboxDetail: components["schemas"]["Mailbox"] & {
- /** @description Host, port and username for connecting a mail client. The PASSWORD is not here and is never returned by this endpoint — create an app password for that. */
- settings: {
- imap: {
- host: string;
- port: number;
- /** @description Transport security, e.g. `SSL/TLS`. */
- security: string;
- /** @description The mailbox address — it is also the login. */
- username: string;
+ responses: {
+ /** @description Updated template */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- smtp: {
- host: string;
- port: number;
- /** @description Transport security, e.g. `SSL/TLS`. */
- security: string;
- /** @description The mailbox address — it is also the login. */
- username: string;
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
};
};
- };
- /** @description RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface. */
- Problem: {
- /** @description Machine-readable lowercase error code, e.g. `scope_missing`. */
- code: string;
- /** @description Explanation specific to this occurrence. */
- detail?: string;
- /** @description Field-level failures. Present on 422 `validation_error` responses. */
- errors?: {
- code: string;
- message: string;
- /** @description RFC 6901 JSON Pointer to the offending field. */
- pointer: string;
- }[];
- /** @description Request path the failure occurred on. */
- instance?: string;
- /** @description Correlation id — quote it in support requests. */
- request_id?: string;
- /** @description HTTP status code, repeated in the body. */
- status: number;
- /** @description Short, stable summary — the same for every occurrence of a `type`. */
- title: string;
- /**
- * Format: uri
- * @description Dereferenceable URI identifying the error class, anchored on the docs errors page.
- */
- type: string;
- };
- ProjectRecord: {
- billingLimitCampaigns: number | null;
- billingLimitInbound: number | null;
- billingLimitTransactional: number | null;
- billingLimitWorkflows: number | null;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- disabled: boolean;
- disabledReason: string | null;
- /** Format: uuid */
- id: string;
- /** @description ISO 639-1 code for customer-facing content. */
- language: string;
- name: string;
- organizationId: string | null;
- /** @description Local-part of the sandbox quick-start sender; null until first derived. */
- sandboxHandle: string | null;
- sesRegion: string | null;
- stripeCustomerId: string | null;
- stripeSubscriptionId: string | null;
- /** @enum {string} */
- tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- };
- /** @description The project the presented credential is scoped to. */
- ProjectV1: {
- /** Format: date-time */
- created_at: string;
- /** @description A disabled project sends nothing; every send is refused. */
- disabled: boolean;
- /** Format: uuid */
- id: string;
- /** @description ISO 639-1 code for customer-facing content. */
- language: string;
- name: string;
- /** @description This project's quick-start sender, usable with no domain setup — but only to the project owner's own verified address, and under a daily cap. Null when none can be derived. */
- sandbox_address: string | null;
- /** @description Locked once the first domain is added. */
- ses_region: string | null;
- /** @enum {string} */
- tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
- };
- /** @description A contact belonging to a segment. */
- SegmentContactV1: {
- /** Format: date-time */
- created_at: string;
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- custom_fields: {
- [key: string]: unknown;
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- email: string;
- /** Format: uuid */
- id: string;
- subscribed: boolean;
};
- /** @description Cursor-paginated list of the contacts belonging to a segment. */
- SegmentContactV1List: {
- data: components["schemas"]["SegmentContactV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ };
+ trackEvent: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description One comparison. `field` addresses a contact column (`email`, `createdAt`, …) or a `customFields.` path. `value` is whatever the operator compares against — its JSON type follows the field, and it is omitted entirely for the presence operators (`exists`, `notExists`). `unit` applies only to the relative-time operators (`within`, `olderThan`, and the `triggered*` family). */
- SegmentFilterV1: {
- field: string;
- /** @enum {string} */
- operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists" | "within" | "olderThan" | "triggered" | "triggeredWithin" | "triggeredOlderThan" | "notTriggered" | "notTriggeredWithin" | "isMemberOf";
- /** @enum {string} */
- unit?: "days" | "hours" | "minutes";
- value?: unknown;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TrackEvent"];
+ };
};
- /** @description A segment as exposed on the v1 API. */
- SegmentV1: {
- condition: components["schemas"]["FilterConditionV1"] | null;
- /** Format: date-time */
- created_at: string;
- description: string | null;
- /** Format: uuid */
- id: string;
- member_count: number;
- name: string;
- track_membership: boolean;
- /** @enum {string} */
- type: "DYNAMIC" | "STATIC";
- /** Format: date-time */
- updated_at: string;
+ responses: {
+ /** @description Event tracked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TrackEventResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`. */
- SegmentV1Create: {
- condition?: components["schemas"]["FilterConditionV1"];
- description?: string;
- name: string;
- /**
- * @description Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition.
- * @default false
- */
- track_membership: boolean;
- /**
- * @default DYNAMIC
- * @enum {string}
- */
- type: "DYNAMIC" | "STATIC";
+ };
+ createProject: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Acknowledgement that a segment was deleted. */
- SegmentV1Deleted: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ requestBody: {
+ content: {
+ "application/json": {
+ name: string;
+ /**
+ * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
+ * @enum {string}
+ */
+ sesRegion?: "us-east-1" | "us-west-2" | "eu-west-1";
+ };
+ };
};
- /** @description Cursor-paginated list of segments. */
- SegmentV1List: {
- data: components["schemas"]["SegmentV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ responses: {
+ /** @description Project created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectRecord"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment. */
- SegmentV1Update: {
- condition?: components["schemas"]["FilterConditionV1"];
- description?: string;
- name?: string;
- track_membership?: boolean;
+ };
+ v1GetCampaignAnalytics: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required. */
- SendEmail: {
- attachments?: {
- content: string;
- contentId?: string;
- contentType: string;
- /**
- * @default attachment
- * @enum {string}
- */
- disposition: "attachment" | "inline";
- filename: string;
- }[];
- bcc?: string[];
- body?: string;
- cc?: string[];
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Campaign statistics */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsCampaignStatsV1"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- from?: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- headers?: {
- [key: string]: string;
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- name?: string;
- /** Format: email */
- reply?: string;
- subject?: string;
- subscribed?: boolean;
- tags?: string[];
- /** Format: uuid */
- template?: string;
- to: string | {
- /** Format: email */
- email: string;
- name?: string;
- } | (string | {
- /** Format: email */
- email: string;
- name?: string;
- })[];
- };
- /** @description Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`. */
- SendEmailData: {
- emails: components["schemas"]["SendEmailRecipientResult"][];
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- timestamp: string;
- };
- /** @description Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient. */
- SendEmailRecipientResult: {
- contact: {
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** Format: uuid */
- email: string;
};
- /** @description Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status. */
- SendEmailResponse: {
- data: components["schemas"]["SendEmailData"];
- /** @enum {boolean} */
- success: true;
+ };
+ v1GetAnalyticsTimeseries: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/emails. Either `template` or `subject`+`body` is required, and `to` names exactly one recipient. */
- SendEmailV1: {
- attachments?: {
- content: string;
- contentId?: string;
- contentType: string;
- /**
- * @default attachment
- * @enum {string}
- */
- disposition: "attachment" | "inline";
- filename: string;
- }[];
- bcc?: string[];
- body?: string;
- cc?: string[];
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Daily time series */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsTimeseriesV1"];
+ };
};
- from?: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- headers?: {
- [key: string]: string;
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- name?: string;
- /** Format: email */
- reply?: string;
- subject?: string;
- subscribed?: boolean;
- tags?: string[];
- /** Format: uuid */
- template?: string;
- /** @description The single recipient. Use `cc`/`bcc` to copy others on the same message. */
- to: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- };
- /** @description Body for POST /api/v1/emails/test. `subject` and `body` are required; `to` defaults to the project owner's verified email, and `from` is refused. */
- SendTestEmailV1: {
- /** @description HTML body. Merge tags are rendered as on any other send. */
- body: string;
- /** @description NOT ACCEPTED. The sender is always this project's sandbox address, resolved server-side; naming one here is refused rather than ignored, so a request that expects a different sender never gets a success it would misread. Read `sandbox_address` from `GET /api/v1/projects` to learn the address, or `from` off this response. */
- from?: string;
- subject: string;
- /**
- * Format: email
- * @description Where to send it. Optional — it defaults to the project owner's own verified account email, which is the only address a sandbox send may reach. Any other value is refused.
- */
- to?: string;
- };
- /** @description Bare success envelope with no payload. */
- SuccessEmpty: {
- /** @enum {boolean} */
- success: true;
- };
- /** @description A single suppressed-email record. */
- Suppression: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- /** @enum {string} */
- source: "SES_WEBHOOK" | "API" | "DASHBOARD";
- };
- /** @description Result of GET /api/suppression/{email} — whether the address is suppressed. */
- SuppressionCheckResponse: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt?: string;
- /** @enum {string} */
- reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- /** @enum {string} */
- source?: "SES_WEBHOOK" | "API" | "DASHBOARD";
- suppressed: boolean;
- };
- /** @description Cursor-paginated list of suppressions. */
- SuppressionListResponse: {
- cursor?: string | null;
- data: components["schemas"]["Suppression"][];
- hasMore?: boolean;
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
- };
- /** @description A reusable email template. */
- Template: {
- body: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- description?: string | null;
- /** Format: email */
- from: string;
- fromName?: string | null;
- /** Format: uuid */
- id: string;
- name: string;
- /** Format: uuid */
- projectId: string;
- /** Format: email */
- replyTo?: string | null;
- subject: string;
- /** @enum {string} */
- type: "MARKETING" | "TRANSACTIONAL" | "HEADLESS";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- };
- /** @description Cursor-paginated list of templates. */
- TemplateListResponse: {
- data: {
- /** @description Cursor for the next page; omitted on the last page. */
- cursor?: string;
- data: components["schemas"]["Template"][];
- hasMore: boolean;
- total: number;
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/track — record a custom event for a contact. */
- TrackEvent: {
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** Format: email */
- email: string;
- event: string;
- subscribed?: boolean;
};
- /** @description Response from POST /api/track. */
- TrackEventResponse: {
- data: {
- /** Format: uuid */
- contact: string;
- /** Format: uuid */
- event: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- timestamp: string;
+ };
+ v1ListTopCampaigns: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ limit?: number;
};
- /** @enum {boolean} */
- success: true;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses. */
- UpdateContactBody: {
- customFields?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Ranked campaigns */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsTopCampaignsV1"];
+ };
};
- subscribed?: boolean;
- };
- /** @description Body for PATCH /api/templates/{id}. */
- UpdateTemplate: {
- body?: string;
- description?: string;
- /** Format: email */
- from?: string;
- fromName?: string | null;
- name?: string;
- /** Format: email */
- replyTo?: string | null;
- subject?: string;
- /** @enum {string} */
- type?: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
- };
- /** @description Body for PATCH /api/webhooks/{id}. */
- UpdateWebhook: {
- eventTypes?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** @enum {string} */
- status?: "ACTIVE" | "PAUSED" | "DISABLED";
- /** Format: uri */
- url?: string;
- };
- /** @description Current email usage against the limits that are actually enforced. */
- UsageV1: {
- daily: {
- /** @description Today's sends. Null when the counter could not be read. */
- emails_sent: number | null;
- limit: number;
- /** @enum {string} */
- trust_tier: "NEW" | "ESTABLISHED" | "TRUSTED";
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- monthly: {
- categories: {
- campaign: {
- emails_sent: number;
- limit: number | null;
- };
- inbound: {
- emails_sent: number;
- limit: number | null;
- };
- transactional: {
- emails_sent: number;
- limit: number | null;
- };
- workflow: {
- emails_sent: number;
- limit: number | null;
- };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
- emails_sent: number;
- /** @description Monthly cap on the total. Null when per-category limits govern instead. */
- limit: number | null;
};
- /**
- * @description `custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`.
- * @enum {string}
- */
- plan: "free" | "pro" | "custom";
- };
- /** @description Body for POST /api/verify — validate email syntax, MX, disposable, etc. */
- VerifyEmail: {
- /** Format: email */
- email: string;
- };
- /** @description Response from POST /api/verify — outcome of the syntax/MX/disposable check. */
- VerifyEmailResponse: {
- data: {
- /** Format: email */
- email: string;
- reason?: string;
- valid: boolean;
- } & {
- [key: string]: unknown;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @enum {boolean} */
- success: true;
};
- /** @description A user-managed outbound webhook. */
- Webhook: {
- consecutiveFailures: number;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- disabledAt?: string | null;
- eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** Format: uuid */
- id: string;
- lastFour?: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- status: "ACTIVE" | "PAUSED" | "DISABLED";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- /** Format: uri */
- url: string;
+ };
+ v1ListCampaigns: {
+ parameters: {
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An attempted webhook delivery. */
- WebhookCall: {
- attempt: number;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- eventType: string;
- /** Format: uuid */
- id: string;
- payload: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Campaign list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1List"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- responseBody?: string | null;
- responseStatus?: number | null;
- /** @enum {string} */
- status: "PENDING" | "SUCCESS" | "FAILED";
- /** Format: uuid */
- webhookId: string;
- };
- /** @description Cursor-paginated list of recent calls for a single webhook. */
- WebhookCallsListResponse: {
- cursor?: string | null;
- data: components["schemas"]["WebhookCall"][];
- hasMore?: boolean;
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
};
- /** @description Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely. */
- WebhookCreateResponse: {
- /** @description A user-managed outbound webhook. */
- data: components["schemas"]["Webhook"] & {
- /** @description Plaintext shared secret. Returned ONCE on create. */
- secret: string;
+ };
+ v1CreateCampaign: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
};
- /** @enum {boolean} */
- success: true;
+ path?: never;
+ cookie?: never;
};
- /** @description Single webhook (no secret). */
- WebhookGetResponse: {
- data: components["schemas"]["Webhook"];
- /** @enum {boolean} */
- success: true;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CampaignV1Create"];
+ };
};
- /** @description List of webhooks for the auth'd project. */
- WebhookListResponse: {
- data: components["schemas"]["Webhook"][];
- /** @enum {boolean} */
- success: true;
+ responses: {
+ /** @description Campaign created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `resource_not_found` — `segment_id` names a segment that does not belong to this project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
};
- /** @description Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once. */
- WebhookRotateSecretResponse: {
- data: {
- /** Format: uuid */
+ };
+ v1GetCampaign: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Resource id. */
id: string;
- /** @description New plaintext shared secret. */
- secret: string;
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/v1/workflows. */
- WorkflowCreateV1: {
- allow_reentry?: boolean;
- description?: string;
- /** @description Workflows are created disabled. A workflow can only be enabled once every step is configured. */
- enabled?: boolean;
- /** @description The custom event that starts this workflow, e.g. `user.signup`. */
- event_name: string;
- name: string;
- };
- /** @description Confirmation that a workflow was deleted. */
- WorkflowDeletedV1: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/workflows/{id}/executions. */
- WorkflowExecutionStartV1: {
- /**
- * Format: uuid
- * @description Contact to enter the workflow. Must belong to this project.
- */
- contact_id: string;
- /** @description Extra variables merged into the contact's data for this run. */
- context?: {
- [key: string]: string | number | boolean | {
- [key: string]: unknown;
- } | unknown[] | null;
+ requestBody?: never;
+ responses: {
+ /** @description The campaign */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1"];
+ };
};
- };
- /** @description One contact's run through a workflow. */
- WorkflowExecutionV1: {
- /** Format: date-time */
- completed_at: string | null;
- /** Format: uuid */
- contact_id: string;
- /** Format: uuid */
- current_step_id: string | null;
- exit_reason: string | null;
- /** Format: uuid */
- id: string;
- /** Format: date-time */
- started_at: string;
- /** @enum {string} */
- status: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
- /** Format: uuid */
- workflow_id: string;
- };
- /** @description Cursor-paginated list of workflow executions, newest first. */
- WorkflowExecutionV1List: {
- data: components["schemas"]["WorkflowExecutionV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
- };
- /** @description Execution, email and conversion totals for one workflow. */
- WorkflowStatsV1: {
- avg_duration_ms: number | null;
- /** @description Execution counts keyed by status; a status with no executions is absent. */
- by_status: {
- [key: string]: number;
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @description Completed ÷ finished executions (0–1). Null until at least one execution has finished. */
- completion_rate: number | null;
- conversions: {
- count: number;
- event_name: string;
- /** Format: uuid */
- goal_id: string;
- name: string;
- }[];
- emails: {
- clicked: number;
- opened: number;
- sent: number;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- total: number;
- /** Format: uuid */
- workflow_id: string;
- };
- /** @description Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409. */
- WorkflowUpdateV1: {
- allow_reentry?: boolean;
- description?: string;
- enabled?: boolean;
- event_name?: string;
- /** @description Per-workflow start rate cap. `null` removes the cap. */
- max_executions_per_hour?: number | null;
- name?: string;
- };
- /** @description An automation workflow as exposed on the v1 API. */
- WorkflowV1: {
- allow_reentry: boolean;
- /** Format: date-time */
- created_at: string;
- description: string | null;
- enabled: boolean;
- /** @description Trigger event for `EVENT` workflows; null for the other trigger types. */
- event_name: string | null;
- /** Format: uuid */
- id: string;
- max_executions_per_hour: number | null;
- name: string;
- /** @enum {string} */
- trigger_type: "EVENT" | "MANUAL" | "SCHEDULE";
- /** Format: date-time */
- updated_at: string;
- /** @description Incremented on every structural (step/transition) change. */
- version: number;
- };
- /** @description Cursor-paginated list of workflows. */
- WorkflowV1List: {
- data: components["schemas"]["WorkflowV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
};
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-interface operations {
- listContacts: {
+ v1DeleteCampaign: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- search?: string;
- subscribed?: "true" | "false";
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact list */
+ /** @description Campaign deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ContactListResponse"];
+ "application/json": components["schemas"]["CampaignV1Deleted"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only `DRAFT` campaigns can be deleted. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createContact: {
+ v1UpdateCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateContact"];
+ "application/json": components["schemas"]["CampaignV1Update"];
};
};
responses: {
- /** @description Contact created */
- 201: {
+ /** @description The updated campaign */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — the campaign is not in an editable status, or the segment change is not allowed. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Email already exists for this project */
- 409: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- bulkCreateContacts: {
+ v1CancelCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ContactBulkCreateBody"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Bulk-create result */
+ /** @description The cancelled campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- created: number;
- errors: {
- index: number;
- message: string;
- }[];
- skipped: number;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- };
- };
- bulkDeleteContacts: {
- parameters: {
- query?: never;
+ };
+ };
+ v1ListCampaignFailures: {
+ parameters: {
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ContactBulkDeleteBody"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Bulk-delete result */
+ /** @description Failed sends */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- deleted: number;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1FailureList"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- upsertContact: {
+ v1PauseCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateContact"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Contact created or updated */
+ /** @description The paused campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `SENDING` campaign can be paused. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getContact: {
+ v1ResumeCampaign: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact */
+ /** @description The resumed campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `PAUSED` campaign can be resumed. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteContact: {
+ v1RetryCampaignFailures: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact deleted */
+ /** @description The retry was queued */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["IdResponse"];
+ "application/json": components["schemas"]["CampaignV1RetryFailed"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `SENT` campaign can have its failed sends retried. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `conflict` — a retry is already running for this campaign. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- updateContact: {
+ v1SendCampaign: {
parameters: {
query?: never;
- header?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
+ };
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
+ requestBody?: {
content: {
- "application/json": components["schemas"]["UpdateContactBody"];
+ "application/json": components["schemas"]["CampaignV1Send"];
};
};
responses: {
- /** @description Updated contact */
+ /** @description The campaign, now `SENDING` or `SCHEDULED` */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listDomains: {
+ v1GetCampaignStats: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Domain list */
+ /** @description Campaign statistics */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DomainListResponse"];
+ "application/json": components["schemas"]["CampaignV1Stats"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- addDomain: {
+ v1ListContacts: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Case-insensitive substring match on the email address. */
+ search?: string;
+ /** @description Filter to subscribed (`true`) or unsubscribed (`false`) contacts. Omit for both. */
+ subscribed?: "true" | "false";
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["AddDomainBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Domain added */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- data: components["schemas"]["Domain"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description Contact list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["ContactV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault — retrying it unchanged will not help. */
- 502: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getDomain: {
+ v1CreateContact: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactV1Create"];
+ };
+ };
responses: {
- /** @description Domain */
- 200: {
+ /** @description The created contact */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Domain"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — a contact with this email already exists in this project. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteDomain: {
+ v1GetContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Domain removed */
+ /** @description The contact */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuccessEmpty"];
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- startDomainSetup: {
+ v1DeleteContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Guided setup session */
+ /** @description Contact deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /**
- * Format: uri
- * @description Open this in a browser to publish the records. Short-lived and domain-specific.
- */
- connectUrl: string;
- /** @description When `connectUrl` stops working. */
- expiresAt: string;
- token: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getDomainVerification: {
+ v1UpdateContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactV1Update"];
+ };
+ };
responses: {
- /** @description Verification status */
+ /** @description The updated contact */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["DomainVerificationStatus"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- verifyDomain: {
+ v1GetContactTopicPreferences: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Verification status */
+ /** @description The contact's preferences */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["DomainVerificationStatus"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactTopicPreferencesV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listEmails: {
+ v1DiagnoseDeliverability: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- tag?: string;
- status?: "PENDING" | "SENT" | "DELIVERED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED";
- from?: string;
+ query: {
+ /** @description A sending domain in this project, e.g. `example.com`. */
+ domain: string;
+ /** @description Optionally, one RECIPIENT address to check as well. Adds its suppression state — the single most common reason a specific person stops receiving mail while everyone else still does. */
+ address?: string;
+ /** @description How far back the delivery counters look. 1–30 days; defaults to 7. */
+ window_days?: number;
};
header?: never;
path?: never;
@@ -4186,688 +11362,605 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Email list */
+ /** @description The diagnosis, with findings */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailListResponse"];
+ "application/json": components["schemas"]["DeliverabilityDiagnosisV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- sendEmail: {
+ v1ListDmarcReports: {
parameters: {
- query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description How far back to read, by the report's window start. 1-180 days; defaults to 30. */
+ days?: number;
+ /** @description Restrict to reports about one of your domains. */
+ domain?: string;
};
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendEmail"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Email accepted / sent */
+ /** @description Cursor-paginated DMARC aggregate reports */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SendEmailResponse"];
- };
- };
- /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DmarcReportV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- sendEmailBatch: {
+ v1ListRecipientDomainStats: {
parameters: {
- query?: never;
- header?: {
- "Idempotency-Key"?: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description How far back to read. 1-30 days; defaults to 30, which is the window the job maintains. */
+ days?: number;
+ /** @description Restrict to one recipient domain. */
+ domain?: string;
};
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["BatchSendBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description All entries sent */
+ /** @description Cursor-paginated recipient-domain rollup */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["BatchSendResponse"];
- };
- };
- /** @description Partial success — at least one entry failed */
- 207: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["BatchSendResponse"];
- };
- };
- /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["RecipientDomainStatsV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getEmail: {
+ v1ListDomains: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Email */
+ /** @description Sending domain list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailGetResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DomainV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- cancelScheduledEmail: {
+ v1CreateDomain: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["DomainV1Create"];
+ };
+ };
responses: {
- /** @description Email cancelled */
- 200: {
+ /** @description The registered sending domain, awaiting DNS */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailGetResponse"];
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — this domain is already registered to a project you can send from. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Email already past PENDING */
- 409: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `internal_error` — AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault: retrying it unchanged will not help. */
+ 502: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- subscribeToList: {
+ v1GetDomain: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description List id. */
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ListSubscribe"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Contact subscribed, or an existing membership returned unchanged */
+ /** @description The sending domain */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ListSubscribeResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- unsubscribeFromList: {
+ v1DeleteDomain: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description List id. */
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ListUnsubscribe"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Contact unsubscribed */
+ /** @description Sending domain removed */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ListUnsubscribeResponse"];
+ "application/json": components["schemas"]["DomainV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — the domain is still in use by a template, workflow step or active campaign. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listMailboxes: {
+ v1VerifyDomain: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Mailbox list */
+ /** @description The sending domain, as SES and DNS now report it */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Mailbox"][];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createMailbox: {
+ v1ValidateEmails: {
parameters: {
query?: never;
header?: never;
@@ -4876,1126 +11969,1066 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateMailboxBody"];
+ "application/json": components["schemas"]["EmailValidationBatchRequestV1"];
};
};
responses: {
- /** @description Mailbox provisioned */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- data: components["schemas"]["Mailbox"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description One verdict per address, in the order they were given */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EmailValidationBatchV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description Resource not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description The address already exists, the domain is not verified, or the project is at its 10-mailbox limit. */
- 409: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Provisioning failed in the mail server. The mailbox row is left `FAILED` and can be retried. */
- 502: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getMailbox: {
+ v1SendEmail: {
parameters: {
query?: never;
- header?: never;
- path: {
- id: string;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
};
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendEmailV1"];
+ };
+ };
responses: {
- /** @description Mailbox with connection settings */
- 200: {
+ /** @description Email queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["MailboxDetail"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EmailV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_disabled`, `domain_not_allowed` — the `from` address does not resolve to a verified domain on this project — or `content_rejected`: automated content review flagged the message and it was not sent. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — `template` names a template that does not belong to this project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — the body did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. Send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `content_review_unavailable` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteMailbox: {
+ v1SendTestEmail: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendTestEmailV1"];
+ };
+ };
responses: {
- /** @description Mailbox deleted */
- 200: {
+ /** @description Test email queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /** @enum {boolean} */
- deleted: true;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EmailTestV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_disabled`, or `forbidden` — the recipient is not the project owner's own verified account email (including the case where that address is not verified yet, so there is no default recipient), or `content_rejected` from automated content review. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — this project has no sandbox sender. The handle is derived from the project owner's email and this project has no owner; no retry fixes it. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — the body did not match the schema, most often because it named a `from`. A test send always comes from the sandbox address. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — the project's daily sandbox send cap is spent. It resets at 00:00 UTC. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `content_review_unavailable` — content review could not run for this new account. Safe to retry. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listAppPasswords: {
+ v1ListEvents: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Return only events with this exact name. */
+ event_name?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description App password list */
+ /** @description Event list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["AppPassword"][];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createAppPassword: {
+ v1TrackEvent: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateAppPassword"];
+ "application/json": components["schemas"]["EventTrackV1"];
};
};
responses: {
- /** @description App password created; the secret is behind the one-time link */
+ /** @description Event recorded */
201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["AppPasswordReveal"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EventV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id in the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- revokeAppPassword: {
+ v1ListEventNames: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- passwordId: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description App password revoked */
+ /** @description Event names */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /** @enum {boolean} */
- revoked: true;
- };
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventNamesV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listApiKeys: {
+ v1GetEventStats: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Project id. */
- id: string;
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description API key list */
+ /** @description Event counts */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ApiKeyListResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventStatsV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createApiKey: {
+ v1ListLists: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Project id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateApiKeyBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description API key created; the secret is behind the reveal link. */
- 201: {
+ /** @description Subscriber lists */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
- data: components["schemas"]["ApiKey"] & {
- /**
- * Format: date-time
- * @description When the reveal link stops working. Create or rotate again to get a new one.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
- */
- revealUrl: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ListV1List"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- revokeApiKey: {
+ v1CreateList: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Project id. */
- id: string;
- /** @description API key id. */
- keyId: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
- responses: {
- /** @description API key revoked */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["SuccessEmpty"];
- };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListV1Create"];
};
- /** @description Validation error */
- 400: {
+ };
+ responses: {
+ /** @description The created list */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- rotateApiKey: {
+ v1GetList: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Project id. */
+ /** @description Resource id. */
id: string;
- /** @description API key id. */
- keyId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description API key rotated; the new secret is behind the reveal link. */
+ /** @description The list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- lastFour: string;
- /**
- * Format: date-time
- * @description When the reveal link stops working. Create or rotate again to get a new one.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
- */
- revealUrl: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listSuppressions: {
+ v1DeleteList: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression list */
+ /** @description List deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuppressionListResponse"];
+ "application/json": components["schemas"]["ListV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- addSuppression: {
+ v1UpdateList: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["AddSuppression"];
+ "application/json": components["schemas"]["ListV1Update"];
};
};
responses: {
- /** @description Suppression added */
- 201: {
+ /** @description The updated list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Suppression"];
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- checkSuppression: {
+ v1StartListValidationRun: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description URL-encoded email address */
- email: string;
+ /** @description Resource id. */
+ id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression check result */
- 200: {
+ /** @description The run, accepted and queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuppressionCheckResponse"];
+ "application/json": components["schemas"]["EmailValidationRunV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- removeSuppression: {
+ v1GetProject: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description URL-encoded email address */
- email: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression removed */
- 204: {
+ /** @description The authenticated project */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["ProjectV1"];
+ };
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — the project was deleted between authentication and this read. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listTemplates: {
+ v1ListSegments: {
parameters: {
query?: {
limit?: number;
- cursor?: string;
- search?: string;
- type?: "MARKETING" | "TRANSACTIONAL" | "HEADLESS";
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
header?: never;
path?: never;
@@ -6003,72 +13036,63 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Template list */
+ /** @description Segment list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["TemplateListResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SegmentV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createTemplate: {
+ v1CreateSegment: {
parameters: {
query?: never;
header?: never;
@@ -6077,512 +13101,491 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateTemplate"];
+ "application/json": components["schemas"]["SegmentV1Create"];
};
};
responses: {
- /** @description Template created */
+ /** @description Segment created */
201: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
+ content: {
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — a `DYNAMIC` segment was submitted without a `condition`. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getTemplate: {
+ v1GetSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Template */
+ /** @description The segment */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteTemplate: {
+ v1DeleteSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Template deleted */
+ /** @description Segment deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["IdResponse"];
+ "application/json": components["schemas"]["SegmentV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — the segment is still used by one or more active campaigns. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Template still in use */
- 409: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- updateTemplate: {
+ v1UpdateSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["UpdateTemplate"];
+ "application/json": components["schemas"]["SegmentV1Update"];
};
};
responses: {
- /** @description Updated template */
+ /** @description The updated segment */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- trackEvent: {
+ v1ListSegmentContacts: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["TrackEvent"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Event tracked */
+ /** @description Segment member list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["TrackEventResponse"];
+ "application/json": components["schemas"]["SegmentContactV1List"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createProject: {
+ v1ListSuppressions: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Filter to one reason. Omit for every suppressed address. */
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": {
- name: string;
- /**
- * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
- * @enum {string}
- */
- sesRegion?: "us-east-1" | "us-west-2" | "eu-west-1";
- };
- };
- };
+ requestBody?: never;
responses: {
- /** @description Project created */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["ProjectRecord"];
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description Suppression list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SuppressionV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- v1GetCampaignAnalytics: {
+ v1CreateSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SuppressionV1Create"];
+ };
+ };
responses: {
- /** @description Campaign statistics */
- 200: {
+ /** @description The suppressed address */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsCampaignStatsV1"];
+ "application/json": components["schemas"]["SuppressionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6632,27 +13635,25 @@ interface operations {
};
};
};
- v1GetAnalyticsTimeseries: {
+ v1GetSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The suppressed address, URL-encoded. */
+ email: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Daily time series */
+ /** @description The suppression record */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsTimeseriesV1"];
+ "application/json": components["schemas"]["SuppressionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6673,6 +13674,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — this address is not suppressed for the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -6702,28 +13712,25 @@ interface operations {
};
};
};
- v1ListTopCampaigns: {
+ v1DeleteSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- limit?: number;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The suppressed address, URL-encoded. */
+ email: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Ranked campaigns */
+ /** @description Address removed from the suppression list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsTopCampaignsV1"];
+ "application/json": components["schemas"]["SuppressionV1Deleted"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6773,12 +13780,15 @@ interface operations {
};
};
};
- v1ListCampaigns: {
+ v1ListTemplates: {
parameters: {
query?: {
limit?: number;
/** @description Opaque cursor from a previous response's `next_cursor`. */
after?: string;
+ /** @description Case-insensitive substring match on the name. */
+ search?: string;
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
};
header?: never;
path?: never;
@@ -6786,13 +13796,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Campaign list */
+ /** @description Template list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1List"];
+ "application/json": components["schemas"]["TemplateV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6842,29 +13852,26 @@ interface operations {
};
};
};
- v1CreateCampaign: {
+ v1CreateTemplate: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Create"];
+ "application/json": components["schemas"]["TemplateV1Create"];
};
};
responses: {
- /** @description Campaign created */
+ /** @description The created template */
201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6885,25 +13892,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — `segment_id` names a segment that does not belong to this project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -6932,7 +13921,7 @@ interface operations {
};
};
};
- v1GetCampaign: {
+ v1GetTemplate: {
parameters: {
query?: never;
header?: never;
@@ -6944,13 +13933,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description The campaign */
+ /** @description The template */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6971,7 +13960,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7009,7 +13998,7 @@ interface operations {
};
};
};
- v1DeleteCampaign: {
+ v1DeleteTemplate: {
parameters: {
query?: never;
header?: never;
@@ -7021,17 +14010,17 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Campaign deleted */
+ /** @description Template deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1Deleted"];
+ "application/json": components["schemas"]["TemplateV1Deleted"];
};
};
- /** @description `validation_error` — only `DRAFT` campaigns can be deleted. */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -7039,8 +14028,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -7048,8 +14037,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -7057,8 +14046,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
+ /** @description `conflict` — the template is still referenced by a workflow step or an active campaign. */
+ 409: {
headers: {
[name: string]: unknown;
};
@@ -7095,7 +14084,7 @@ interface operations {
};
};
};
- v1UpdateCampaign: {
+ v1UpdateTemplate: {
parameters: {
query?: never;
header?: never;
@@ -7107,26 +14096,17 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Update"];
+ "application/json": components["schemas"]["TemplateV1Update"];
};
};
responses: {
- /** @description The updated campaign */
+ /** @description The updated template */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — the campaign is not in an editable status, or the segment change is not allowed. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7147,7 +14127,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7185,34 +14165,27 @@ interface operations {
};
};
};
- v1CancelCampaign: {
+ v1ListTopics: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Resource id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ include_archived?: boolean | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The cancelled campaign */
+ /** @description One page of topics */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicListV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7233,15 +14206,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7271,34 +14235,26 @@ interface operations {
};
};
};
- v1PauseCampaign: {
+ v1CreateTopic: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Resource id. */
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
- responses: {
- /** @description The paused campaign */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TopicCreateV1"];
};
- /** @description `validation_error` — only a `SENDING` campaign can be paused. */
- 400: {
+ };
+ responses: {
+ /** @description The created topic */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7319,15 +14275,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7357,7 +14304,7 @@ interface operations {
};
};
};
- v1ResumeCampaign: {
+ v1GetTopic: {
parameters: {
query?: never;
header?: never;
@@ -7369,22 +14316,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description The resumed campaign */
+ /** @description The topic */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — only a `PAUSED` campaign can be resumed. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7405,7 +14343,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7443,41 +14381,29 @@ interface operations {
};
};
};
- v1SendCampaign: {
+ v1UpdateTopic: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path: {
/** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody?: {
+ requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Send"];
+ "application/json": components["schemas"]["TopicUpdateV1"];
};
};
responses: {
- /** @description The campaign, now `SENDING` or `SCHEDULED` */
+ /** @description The updated topic */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7498,7 +14424,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7507,16 +14433,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7545,7 +14462,7 @@ interface operations {
};
};
};
- v1GetCampaignStats: {
+ v1SetTopicSubscription: {
parameters: {
query?: never;
header?: never;
@@ -7555,15 +14472,19 @@ interface operations {
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TopicSubscribeV1"];
+ };
+ };
responses: {
- /** @description Campaign statistics */
+ /** @description The resulting subscription */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1Stats"];
+ "application/json": components["schemas"]["TopicSubscriptionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7584,7 +14505,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7622,29 +14543,22 @@ interface operations {
};
};
};
- v1SendEmail: {
+ v1GetUsage: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendEmailV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Email queued */
- 202: {
+ /** @description Current usage */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailV1"];
+ "application/json": components["schemas"]["UsageV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7656,7 +14570,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_disabled`, `domain_not_allowed` — the `from` address does not resolve to a verified domain on this project — or `content_rejected`: automated content review flagged the message and it was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
@@ -7665,25 +14579,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — `template` names a template that does not belong to this project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the body did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. Send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7710,37 +14606,27 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `content_review_unavailable` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
};
};
- v1SendTestEmail: {
+ v1GetValidationRun: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendTestEmailV1"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Test email queued */
- 202: {
+ /** @description The run */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailTestV1"];
+ "application/json": components["schemas"]["EmailValidationRunV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7752,7 +14638,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_disabled`, or `forbidden` — the recipient is not the project owner's own verified account email (including the case where that address is not verified yet, so there is no default recipient), or `content_rejected` from automated content review. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
@@ -7761,8 +14647,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — this project has no sandbox sender. The handle is derived from the project owner's email and this project has no owner; no retry fixes it. */
- 409: {
+ /** @description `resource_not_found` — no validation run with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -7770,7 +14656,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `validation_error` — the body did not match the schema, most often because it named a `from`. A test send always comes from the sandbox address. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7779,7 +14665,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `rate_limited` — the project's daily sandbox send cap is spent. It resets at 00:00 UTC. */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
@@ -7797,39 +14683,33 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `content_review_unavailable` — content review could not run for this new account. Safe to retry. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
};
};
- v1ListEvents: {
+ v1ListValidationRunResults: {
parameters: {
query?: {
limit?: number;
/** @description Opaque cursor from a previous response's `next_cursor`. */
after?: string;
- /** @description Return only events with this exact name. */
- event_name?: string;
+ /** @description Return only results with this verdict — `undeliverable` is the usual filter. */
+ verdict?: components["schemas"]["EmailValidationVerdictV1"] & unknown;
};
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Event list */
+ /** @description One page of results */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventV1List"];
+ "application/json": components["schemas"]["EmailValidationResultListV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7850,6 +14730,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no validation run with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7879,26 +14768,26 @@ interface operations {
};
};
};
- v1TrackEvent: {
+ v1ListWebhooks: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["EventTrackV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Event recorded */
- 201: {
+ /** @description Webhook list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventV1"];
+ "application/json": components["schemas"]["WebhookV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7919,15 +14808,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no contact with this id in the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7957,22 +14837,26 @@ interface operations {
};
};
};
- v1ListEventNames: {
+ v1CreateWebhook: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookV1Create"];
+ };
+ };
responses: {
- /** @description Event names */
- 200: {
+ /** @description The created webhook and its one-time signing secret */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventNamesV1"];
+ "application/json": components["schemas"]["WebhookV1Created"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8022,27 +14906,25 @@ interface operations {
};
};
};
- v1GetEventStats: {
+ v1GetWebhook: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Event counts */
+ /** @description The webhook */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventStatsV1"];
+ "application/json": components["schemas"]["WebhookV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8063,6 +14945,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8092,22 +14983,25 @@ interface operations {
};
};
};
- v1GetProject: {
+ v1DeleteWebhook: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The authenticated project */
+ /** @description Webhook deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ProjectV1"];
+ "application/json": components["schemas"]["WebhookV1Deleted"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8128,7 +15022,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — the project was deleted between authentication and this read. */
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8166,26 +15060,29 @@ interface operations {
};
};
};
- v1ListSegments: {
+ v1UpdateWebhook: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookV1Update"];
+ };
+ };
responses: {
- /** @description Segment list */
+ /** @description The updated webhook */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1List"];
+ "application/json": components["schemas"]["WebhookV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8206,6 +15103,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8235,30 +15141,29 @@ interface operations {
};
};
};
- v1CreateSegment: {
+ v1RotateWebhookSecret: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["SegmentV1Create"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Segment created */
- 201: {
+ /** @description The new signing secret and the moment the previous one stops verifying */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
+ "application/json": components["schemas"]["WebhookV1SecretRotated"];
};
};
- /** @description `validation_error` — a `DYNAMIC` segment was submitted without a `condition`. */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -8266,8 +15171,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -8275,8 +15180,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -8313,38 +15218,30 @@ interface operations {
};
};
};
- v1GetSegment: {
+ v1ListWorkflows: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Resource id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The segment */
+ /** @description Workflow list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
- };
- };
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["WorkflowV1List"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -8352,8 +15249,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
- 404: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -8390,25 +15287,26 @@ interface operations {
};
};
};
- v1DeleteSegment: {
+ v1CreateWorkflow: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Resource id. */
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowCreateV1"];
+ };
+ };
responses: {
- /** @description Segment deleted */
- 200: {
+ /** @description Workflow created */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1Deleted"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8429,24 +15327,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — the segment is still used by one or more active campaigns. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8476,29 +15356,25 @@ interface operations {
};
};
};
- v1UpdateSegment: {
+ v1CancelWorkflowExecution: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Resource id. */
- id: string;
+ /** @description Workflow execution id. */
+ execution_id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SegmentV1Update"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description The updated segment */
+ /** @description Cancelled execution */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8519,7 +15395,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no execution with this id in the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8557,29 +15433,25 @@ interface operations {
};
};
};
- v1ListSegmentContacts: {
+ v1GetWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
path: {
- /** @description Resource id. */
+ /** @description Workflow id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Segment member list */
+ /** @description Workflow */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentContactV1List"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8600,7 +15472,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8638,22 +15510,25 @@ interface operations {
};
};
};
- v1GetUsage: {
+ v1DeleteWorkflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Current usage */
+ /** @description Workflow deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UsageV1"];
+ "application/json": components["schemas"]["WorkflowDeletedV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8674,6 +15549,24 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — the workflow still has running executions. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8703,26 +15596,29 @@ interface operations {
};
};
};
- v1ListWorkflows: {
+ v1UpdateWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowUpdateV1"];
+ };
+ };
responses: {
- /** @description Workflow list */
+ /** @description Updated workflow */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1List"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8743,6 +15639,24 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — the trigger cannot be changed while executions are running. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8772,20 +15686,23 @@ interface operations {
};
};
};
- v1CreateWorkflow: {
+ v1CloneWorkflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody: {
+ requestBody?: {
content: {
- "application/json": components["schemas"]["WorkflowCreateV1"];
+ "application/json": components["schemas"]["WorkflowCloneV1"];
};
};
responses: {
- /** @description Workflow created */
+ /** @description The cloned workflow */
201: {
headers: {
[name: string]: unknown;
@@ -8812,6 +15729,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8841,25 +15767,31 @@ interface operations {
};
};
};
- v1CancelWorkflowExecution: {
+ v1ListWorkflowExecutions: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Return only executions in this state. */
+ status?: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
+ };
header?: never;
path: {
- /** @description Workflow execution id. */
- execution_id: string;
+ /** @description Workflow id. */
+ id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Cancelled execution */
+ /** @description Execution list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8880,7 +15812,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no execution with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8918,7 +15850,7 @@ interface operations {
};
};
};
- v1GetWorkflow: {
+ v1StartWorkflowExecution: {
parameters: {
query?: never;
header?: never;
@@ -8928,15 +15860,19 @@ interface operations {
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowExecutionStartV1"];
+ };
+ };
responses: {
- /** @description Workflow */
- 200: {
+ /** @description Execution started */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8957,7 +15893,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no such workflow, or no such contact in this project. */
404: {
headers: {
[name: string]: unknown;
@@ -8966,6 +15902,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `conflict` — the contact already has an execution and re-entry is not allowed. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8995,7 +15940,7 @@ interface operations {
};
};
};
- v1DeleteWorkflow: {
+ v1GetWorkflowGraph: {
parameters: {
query?: never;
header?: never;
@@ -9007,13 +15952,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Workflow deleted */
+ /** @description The workflow's graph */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowDeletedV1"];
+ "application/json": components["schemas"]["WorkflowGraphV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9034,7 +15979,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9043,15 +15988,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the workflow still has running executions. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -9081,7 +16017,7 @@ interface operations {
};
};
};
- v1UpdateWorkflow: {
+ v1ReplaceWorkflowGraph: {
parameters: {
query?: never;
header?: never;
@@ -9093,17 +16029,17 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["WorkflowUpdateV1"];
+ "application/json": components["schemas"]["WorkflowGraphReplaceV1"];
};
};
responses: {
- /** @description Updated workflow */
+ /** @description The graph as it now stands */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1"];
+ "application/json": components["schemas"]["WorkflowGraphV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9124,7 +16060,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9133,7 +16069,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the trigger cannot be changed while executions are running. */
+ /** @description `conflict` — the workflow has running executions, or an id in the document already belongs to a different workflow. */
409: {
headers: {
[name: string]: unknown;
@@ -9171,15 +16107,9 @@ interface operations {
};
};
};
- v1ListWorkflowExecutions: {
+ v1PauseWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- /** @description Return only executions in this state. */
- status?: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
- };
+ query?: never;
header?: never;
path: {
/** @description Workflow id. */
@@ -9189,13 +16119,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Execution list */
+ /** @description The workflow, and the number of runs this call cancelled */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1List"];
+ "application/json": components["schemas"]["WorkflowStateChangeV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9216,7 +16146,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9254,7 +16184,7 @@ interface operations {
};
};
};
- v1StartWorkflowExecution: {
+ v1ResumeWorkflow: {
parameters: {
query?: never;
header?: never;
@@ -9264,19 +16194,15 @@ interface operations {
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["WorkflowExecutionStartV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Execution started */
- 201: {
+ /** @description The workflow, with `cancelled_executions` always 0 */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1"];
+ "application/json": components["schemas"]["WorkflowStateChangeV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9289,16 +16215,7 @@ interface operations {
};
};
/** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `resource_not_found` — no such workflow, or no such contact in this project. */
- 404: {
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -9306,8 +16223,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the contact already has an execution and re-entry is not allowed. */
- 409: {
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -9386,7 +16303,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -10009,7 +16926,23 @@ type BatchSendResponse = components["schemas"]["BatchSendResponse"];
type BatchEntryResult = components["schemas"]["BatchEntryResult"];
type EmailRecord = components["schemas"]["Email"];
type EmailListResponse = components["schemas"]["EmailListResponse"];
-type EmailGetResponse = components["schemas"]["EmailGetResponse"];
+/**
+ * One transition in a message's delivery history — the append-only record
+ * behind `status`. `status` says where the message is now; these say how it got
+ * there.
+ */
+type EmailEvent = components["schemas"]["EmailEvent"];
+/** An email together with its delivery history, oldest first. */
+type EmailWithEvents = components["schemas"]["EmailWithEvents"];
+/**
+ * A single email with no history — what `emails.cancelSchedule` resolves.
+ *
+ * Was `EmailGetResponse` in 1.0, which named the operation rather than the
+ * shape and was then reused by an operation that is not a GET.
+ */
+type EmailResponse = components["schemas"]["EmailResponse"];
+/** `emails.get` — one email plus its delivery events. */
+type EmailDetailResponse = components["schemas"]["EmailDetailResponse"];
type ListEmailsQuery = NonNullable;
type ContactRecord = components["schemas"]["Contact"];
type ContactListResponse = components["schemas"]["ContactListResponse"];
@@ -10021,6 +16954,8 @@ type ListContactsQuery = NonNullable;
type SuppressionRecord = components["schemas"]["Suppression"];
type SuppressionListResponse = components["schemas"]["SuppressionListResponse"];
type SuppressionCheckResponse = components["schemas"]["SuppressionCheckResponse"];
@@ -10069,8 +17020,8 @@ type CampaignV1 = components["schemas"]["CampaignV1"];
type CampaignListV1 = components["schemas"]["CampaignV1List"];
type CampaignDeletedV1 = components["schemas"]["CampaignV1Deleted"];
type CampaignStatsV1 = components["schemas"]["CampaignV1Stats"];
-/** `type` defaults to `MARKETING` server-side, so it is optional here. */
-type CreateCampaignV1Request = PartialKeys;
+/** `email_category` defaults to `MARKETING` server-side, so it is optional here. */
+type CreateCampaignV1Request = PartialKeys;
type UpdateCampaignV1Request = components["schemas"]["CampaignV1Update"];
type SendCampaignV1Request = components["schemas"]["CampaignV1Send"];
type ListCampaignsV1Query = NonNullable;
@@ -10116,6 +17067,92 @@ type EmailTestV1 = components["schemas"]["EmailTestV1"];
type AnalyticsTimeseriesV1Query = NonNullable;
type AnalyticsCampaignsV1Query = NonNullable;
type ListTopCampaignsV1Query = NonNullable;
+type ContactV1 = components["schemas"]["ContactV1"];
+type ContactListV1 = components["schemas"]["ContactV1List"];
+type ContactDeletedV1 = components["schemas"]["ContactV1Deleted"];
+/** `subscribed` defaults to `true` server-side, so it is optional here. */
+type CreateContactV1Request = PartialKeys;
+type UpdateContactV1Request = components["schemas"]["ContactV1Update"];
+/** Everything one contact has said they want, topic by topic. */
+type ContactTopicPreferencesV1 = components["schemas"]["ContactTopicPreferencesV1"];
+type ListContactsV1Query = NonNullable;
+type ListV1 = components["schemas"]["ListV1"];
+type ListListV1 = components["schemas"]["ListV1List"];
+type ListDeletedV1 = components["schemas"]["ListV1Deleted"];
+/** `double_opt_in` defaults to `false` server-side, so it is optional here. */
+type CreateListV1Request = PartialKeys;
+type UpdateListV1Request = components["schemas"]["ListV1Update"];
+type ListListsV1Query = NonNullable;
+type TemplateV1 = components["schemas"]["TemplateV1"];
+type TemplateListV1 = components["schemas"]["TemplateV1List"];
+type TemplateDeletedV1 = components["schemas"]["TemplateV1Deleted"];
+/** `email_category` defaults to `MARKETING` server-side, so it is optional here. */
+type CreateTemplateV1Request = PartialKeys;
+type UpdateTemplateV1Request = components["schemas"]["TemplateV1Update"];
+type ListTemplatesV1Query = NonNullable;
+type DomainV1 = components["schemas"]["DomainV1"];
+type DomainListV1 = components["schemas"]["DomainV1List"];
+type DomainDeletedV1 = components["schemas"]["DomainV1Deleted"];
+type CreateDomainV1Request = components["schemas"]["DomainV1Create"];
+type ListDomainsV1Query = NonNullable;
+type WebhookV1 = components["schemas"]["WebhookV1"];
+type WebhookListV1 = components["schemas"]["WebhookV1List"];
+type WebhookDeletedV1 = components["schemas"]["WebhookV1Deleted"];
+/** The create response, and the only time the signing secret is readable. */
+type WebhookCreatedV1 = components["schemas"]["WebhookV1Created"];
+/** Rotation answers the new secret once, for the same reason. */
+type WebhookSecretRotatedV1 = components["schemas"]["WebhookV1SecretRotated"];
+type CreateWebhookV1Request = components["schemas"]["WebhookV1Create"];
+type UpdateWebhookV1Request = components["schemas"]["WebhookV1Update"];
+type ListWebhooksV1Query = NonNullable;
+type SuppressionV1 = components["schemas"]["SuppressionV1"];
+type SuppressionListV1 = components["schemas"]["SuppressionV1List"];
+type SuppressionDeletedV1 = components["schemas"]["SuppressionV1Deleted"];
+/** `reason` defaults to `MANUAL` server-side, so it is optional here. */
+type CreateSuppressionV1Request = PartialKeys;
+type ListSuppressionsV1Query = NonNullable;
+type TopicV1 = components["schemas"]["TopicV1"];
+type TopicListV1 = components["schemas"]["TopicListV1"];
+type CreateTopicV1Request = components["schemas"]["TopicCreateV1"];
+type UpdateTopicV1Request = components["schemas"]["TopicUpdateV1"];
+type SetTopicSubscriptionV1Request = components["schemas"]["TopicSubscribeV1"];
+type TopicSubscriptionV1 = components["schemas"]["TopicSubscriptionV1"];
+type TopicSubscriptionStatusV1 = components["schemas"]["TopicSubscriptionStatusV1"];
+type ListTopicsV1Query = NonNullable;
+type ValidateEmailsV1Request = components["schemas"]["EmailValidationBatchRequestV1"];
+type EmailValidationBatchV1 = components["schemas"]["EmailValidationBatchV1"];
+type EmailValidationV1 = components["schemas"]["EmailValidationV1"];
+type EmailValidationVerdictV1 = components["schemas"]["EmailValidationVerdictV1"];
+type EmailValidationRunV1 = components["schemas"]["EmailValidationRunV1"];
+type EmailValidationResultListV1 = components["schemas"]["EmailValidationResultListV1"];
+/**
+ * One address's verdict inside a run's results — a validation plus the
+ * `contact_id` it came from. The spec composes it inline rather than naming a
+ * component, so it is read off the page it appears in.
+ */
+type EmailValidationResultV1 = EmailValidationResultListV1["data"][number];
+type ListValidationResultsV1Query = NonNullable;
+type DeliverabilityDiagnosisV1 = components["schemas"]["DeliverabilityDiagnosisV1"];
+type DeliverabilityFindingV1 = components["schemas"]["DeliverabilityFindingV1"];
+type DeliverabilityFindingSeverityV1 = components["schemas"]["DeliverabilityFindingSeverityV1"];
+type DeliverabilityIdentityV1 = components["schemas"]["DeliverabilityIdentityV1"];
+type DeliverabilityRecentDeliveryV1 = components["schemas"]["DeliverabilityRecentDeliveryV1"];
+type DeliverabilitySuppressionV1 = components["schemas"]["DeliverabilitySuppressionV1"];
+type RecipientDomainStatsV1 = components["schemas"]["RecipientDomainStatsV1"];
+type RecipientDomainStatsListV1 = components["schemas"]["RecipientDomainStatsV1List"];
+type DmarcReportV1 = components["schemas"]["DmarcReportV1"];
+type DmarcReportListV1 = components["schemas"]["DmarcReportV1List"];
+type DiagnoseDeliverabilityV1Query = NonNullable;
+type ListRecipientDomainStatsV1Query = NonNullable;
+type ListDmarcReportsV1Query = NonNullable;
+type CampaignFailureV1 = components["schemas"]["CampaignV1Failure"];
+type CampaignFailureListV1 = components["schemas"]["CampaignV1FailureList"];
+type CampaignRetryFailedV1 = components["schemas"]["CampaignV1RetryFailed"];
+type ListCampaignFailuresV1Query = NonNullable;
+type WorkflowGraphV1 = components["schemas"]["WorkflowGraphV1"];
+type ReplaceWorkflowGraphV1Request = components["schemas"]["WorkflowGraphReplaceV1"];
+type CloneWorkflowV1Request = components["schemas"]["WorkflowCloneV1"];
+type WorkflowStateChangeV1 = components["schemas"]["WorkflowStateChangeV1"];
/**
* Sending analytics on the `/api/v1` surface.
@@ -10200,8 +17237,43 @@ declare class CampaignsResource {
resume(id: string): Promise;
/** Delivery and engagement counters plus derived rates for one campaign. */
stats(id: string): Promise;
+ /**
+ * The recipients this campaign did not reach, and why.
+ *
+ * {@link stats} says how many sends failed; only this says who. `reason`
+ * comes from a fixed vocabulary rather than the underlying error text, so it
+ * is stable enough to branch on — and it is `null` on rows recorded before
+ * reasons were captured.
+ *
+ * Cursor-paginated like every other v1 list, but uniquely it also carries
+ * `total`: {@link retryFailed} acts on that number, and `has_more` alone
+ * cannot tell you whether 3 or 30,000 sends failed.
+ */
+ listFailures(id: string, query?: ListCampaignFailuresV1Query): Promise;
+ /** Iterate every failed send across pages, yielding one recipient at a time. */
+ listFailuresAll(id: string, query?: ListCampaignFailuresV1Query): AsyncGenerator;
+ /**
+ * Re-drive only the recipients whose send failed. Nobody who already received
+ * the campaign is mailed a second time — each ledger row is claimed before it
+ * is touched, and a row whose email exists already is re-queued, not re-sent.
+ *
+ * The walk runs in the background, so this resolves as soon as it is queued,
+ * reporting `queued`: how many failed rows it was started for. Only a `SENT`
+ * campaign qualifies (`400 validation_error` otherwise), and a retry already
+ * running answers `409 conflict`. Takes no body.
+ */
+ retryFailed(id: string): Promise;
}
+/**
+ * Contacts, on both surfaces.
+ *
+ * The unsuffixed methods speak the legacy `/api/*` dialect — camelCase bodies
+ * inside a `{ success, data }` envelope the SDK unwraps. The `V1`-suffixed
+ * methods speak `/api/v1`: bare snake_case bodies, cursor pagination, and
+ * RFC 9457 problem documents on error. Both answer the same questions, so the
+ * suffix is there to keep a call site from confusing one for the other.
+ */
declare class ContactsResource {
private readonly client;
constructor(client: Sendly);
@@ -10221,8 +17293,138 @@ declare class ContactsResource {
update(id: string, body: UpdateContactRequest): Promise;
/** Delete a contact. The API answers 200 with `{ success, data: { id } }`; the SDK resolves void. */
delete(id: string): Promise;
+ /**
+ * List contacts on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after` with no total count, narrowed by
+ * `search` (case-insensitive substring on the address) and `subscribed`.
+ * Hold the filters steady for the whole walk — the cursor encodes them, and
+ * changing one mid-pagination returns `422 validation_error` asking you to
+ * restart. {@link listAllV1} drives the loop for you.
+ */
+ listV1(query?: ListContactsV1Query): Promise;
+ /** Iterate every v1 contact across pages, yielding one contact at a time. */
+ listAllV1(query?: ListContactsV1Query): AsyncGenerator;
+ /**
+ * Create a contact. Only `email` is required — `subscribed` defaults to true
+ * server-side, and `custom_fields` is arbitrary JSON that templates can read
+ * back as `{{ variables }}`.
+ */
+ createV1(body: CreateContactV1Request): Promise;
+ /**
+ * Retrieve a single contact by id. v1 has no lookup-by-address route — reach
+ * a contact you only know the email of through {@link listV1}'s `search`.
+ */
+ getV1(id: string): Promise;
+ /**
+ * Patch a contact. Only the fields you send are changed, with two caveats.
+ *
+ * `email` is not patchable at all: an address is the contact's identity here,
+ * and rewriting it in place would change who every earlier send was addressed
+ * to. Create the new address instead.
+ *
+ * `custom_fields` is **replaced, not merged** — the object you send becomes
+ * the whole of it, so read the contact and send back every key you mean to
+ * keep. Sending a partial object silently drops the rest.
+ */
+ updateV1(id: string, body: UpdateContactV1Request): Promise;
+ /**
+ * Delete a contact. Unlike the legacy {@link delete}, this resolves the
+ * `{ id, deleted }` acknowledgement rather than discarding it.
+ */
+ deleteV1(id: string): Promise;
+ /**
+ * Read everything this contact has said about what they want.
+ *
+ * The top-level `subscribed` is the global marketing opt-out and outranks
+ * every topic: false means nothing marketing reaches them whatever the topic
+ * rows say. Each topic's own `subscribed` is the effective answer the send
+ * path reaches today, with the topic's `default_opt_in` already folded in, so
+ * a contact who has never answered still reads correctly.
+ */
+ topicPreferences(id: string): Promise;
+}
+
+/**
+ * Deliverability on the `/api/v1` surface — why mail from your domains is, or
+ * is not, arriving.
+ *
+ * Responses are bare v1 bodies (no `{ success, data }` envelope) and errors are
+ * RFC 9457 problem documents.
+ */
+declare class DeliverabilityResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * Diagnose one of your SENDING domains: its DNS identity, the project's
+ * recent delivery outcomes, optionally one recipient's suppression state, and
+ * the `findings` drawn from them, worst first. Branch on a finding's `code`,
+ * never on its prose.
+ *
+ * `query.domain` is required — the endpoint answers about one domain. The
+ * optional `address` is a RECIPIENT to check alongside it, because being
+ * suppressed is the single most common reason one person stops receiving mail
+ * while everyone else still does. `window_days` (1–30, default 7) only moves
+ * the delivery counters.
+ *
+ * Nothing here is looked up live: the DNS statuses are the verification
+ * refresh job's cached results, and `identity.last_checked_at` says when they
+ * were filled. `recent_delivery` is project-wide rather than per-domain — its
+ * own `scope` field says so — because an email row records no sending domain.
+ */
+ diagnose(query: DiagnoseDeliverabilityV1Query): Promise;
+ /**
+ * Delivery outcomes broken out by RECIPIENT domain and UTC day, newest day
+ * first.
+ *
+ * These are the domains you send TO — `gmail.com`, `outlook.com` — not the
+ * domains you send FROM. That is the axis {@link diagnose} cannot report: its
+ * project-wide rates hide the case that matters most, one recipient domain
+ * refusing nearly everything while the rest of your mail is healthy.
+ *
+ * Cursor-paginated on `limit` + `after`. The counts come from an hourly
+ * rollup job over a rolling 30-day window, not from a query run on request;
+ * each row's `computed_at` says when it was last rebuilt. No rate is
+ * published, because a rate over three sends is not information.
+ */
+ listDomainStats(query?: ListRecipientDomainStatsV1Query): Promise;
+ /** Iterate every recipient-domain row across pages, one day-and-domain at a time. */
+ listDomainStatsAll(query?: ListRecipientDomainStatsV1Query): AsyncGenerator;
+ /**
+ * DMARC aggregate (RUA) reports that receiving providers have sent about your
+ * domains, newest reporting window first. Cursor-paginated on `limit` +
+ * `after`.
+ *
+ * An empty list is the correct answer, not a bug, until a policy domain is
+ * registered in this project and its DMARC record names an address we
+ * receive: only reports about a registered domain are stored, and receivers
+ * send them on their own schedule (typically once a day).
+ *
+ * `intake_configured` says which kind of empty you are looking at. When it is
+ * `false` this deployment has no DMARC report intake mailbox at all, so no
+ * report can ever arrive and an empty `data` means the feature is off — not
+ * that your domains are clean. The two are otherwise indistinguishable, so
+ * read the flag before reporting "no DMARC failures" to anyone.
+ *
+ * `pass_count` counts DMARC ALIGNMENT taken from `policy_evaluated`, not raw
+ * authentication results — a message can pass SPF for a domain that is not
+ * the one in its From header, which is exactly the case DMARC exists to
+ * catch.
+ */
+ listDmarcReports(query?: ListDmarcReportsV1Query): Promise;
+ /** Iterate every DMARC report across pages, one report at a time. */
+ listDmarcReportsAll(query?: ListDmarcReportsV1Query): AsyncGenerator;
}
+/**
+ * Sending domains, on both surfaces.
+ *
+ * The unsuffixed methods speak the legacy `/api/*` dialect — camelCase bodies
+ * inside a `{ success, data }` envelope the SDK unwraps. The `V1`-suffixed
+ * methods speak `/api/v1`: bare snake_case bodies, cursor pagination, and
+ * RFC 9457 problem documents on error. Both answer the same questions, so the
+ * suffix is there to keep a call site from confusing one for the other.
+ */
declare class DomainsResource {
private readonly client;
constructor(client: Sendly);
@@ -10233,14 +17435,24 @@ declare class DomainsResource {
* `eu-west-1`). On the very first domain for a project this also locks the
* project's region; subsequent calls must match.
*
- * The response includes DNS records to set.
+ * The response carries `dkimTokens` — the SES DKIM tokens to publish as
+ * CNAME records before the domain can verify — alongside `dkimStatus`,
+ * `spfStatus` and `dmarcStatus`, each the result of the last DNS check for
+ * that record type.
*/
create(body: AddDomainRequest): Promise;
/** List all domains for the project. */
list(): Promise;
/** Fetch a single domain. */
get(id: string): Promise;
- /** Trigger SES verification for a domain. */
+ /**
+ * Trigger SES verification for a domain.
+ *
+ * `status` is SES's own raw DKIM verification state (`Success`, `Pending`),
+ * while `dkimStatus`, `spfStatus` and `dmarcStatus` are this platform's own
+ * DNS check per record type. `tokens` carries the DKIM tokens SES has still
+ * to report and is absent once verification has resolved.
+ */
verify(id: string): Promise;
/** Read current SES verification status for a domain. */
getVerification(id: string): Promise;
@@ -10254,8 +17466,77 @@ declare class DomainsResource {
* back the link, not to model the flow behind it.
*/
startSetup(id: string): Promise;
+ /**
+ * Assign this sending identity to transactional or marketing traffic.
+ *
+ * Streams are enforced, not labelled: once assigned, a send of the other kind
+ * from this identity is refused with 403 — which is what keeps a campaign's
+ * complaint rate off the identity your password resets go out on. Pass
+ * `stream: null` to unassign, returning it to carrying both.
+ *
+ * `streamDefault` demotes whichever identity currently holds the default for
+ * that stream, and `defaultFromAddress` has to be an address on this
+ * identity's own host. Every field is optional; an omitted one is left alone.
+ *
+ * Legacy dialect: camelCase body, and the updated domain arrives inside the
+ * `{ success, data }` envelope this method unwraps for you.
+ */
+ assignStream(id: string, body: AssignDomainStreamRequest): Promise;
/** Delete a domain. */
delete(id: string): Promise;
+ /**
+ * List sending domains, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. {@link listAllV1}
+ * drives the loop for you.
+ *
+ * `verified` is SES's verdict on the identity and is what decides whether mail
+ * can leave from this domain; `dkim_verified` is a separate fact — what the
+ * DNS health refresh last read for the DKIM records — so the two disagree
+ * while a re-check is in flight and neither is a spelling of the other.
+ */
+ listV1(query?: ListDomainsV1Query): Promise;
+ /** Iterate every sending domain across pages, yielding one domain at a time. */
+ listAllV1(query?: ListDomainsV1Query): AsyncGenerator;
+ /**
+ * Register a sending domain and start SES DKIM verification.
+ *
+ * The identity comes back with `verified: false` — nothing is verified until
+ * the DKIM records are published in the domain's own DNS and SES resolves
+ * them, so poll {@link verifyV1} after publishing them.
+ *
+ * The first domain a project adds LOCKS the project's SES `region`; every
+ * later domain must match it. `stream_default` requires `stream`, and sending
+ * it alone is answered with `422 validation_error` rather than ignored.
+ */
+ createV1(body: CreateDomainV1Request): Promise;
+ /** Retrieve a single sending domain. */
+ getV1(id: string): Promise;
+ /**
+ * Re-read the domain's state from SES and DNS, and resolve the refreshed
+ * document.
+ *
+ * This does not verify anything and changes none of the domain's own fields.
+ * Verification happens in the domain's DNS, when its owner publishes the DKIM
+ * records SES minted at creation, and Amazon decides when those resolve. What
+ * this call does is ask SES what it currently sees, re-check SPF and DMARC,
+ * and persist that answer — so a caller polling after a DNS change learns the
+ * outcome without waiting for the periodic sweep. Calling it on a domain whose
+ * records are not published yet is not an error and does not hurry anything.
+ *
+ * A POST rather than a GET because the refreshed state is persisted and a
+ * verified/unverified transition notifies the project.
+ */
+ verifyV1(id: string): Promise;
+ /**
+ * Remove a sending domain. Resolves `{ id, deleted }`.
+ *
+ * Refused with `409 conflict` while a template, workflow step or active
+ * campaign still sends from an address on this host. The SES identity goes
+ * too unless another project holds the same host — and its DKIM keys with it,
+ * so re-adding later mints records that must be published again.
+ */
+ deleteV1(id: string): Promise;
}
declare class EmailsResource {
@@ -10301,10 +17582,24 @@ declare class EmailsResource {
batch(body: BatchSendRequest, opts?: IdempotencyOptions): Promise;
/** List emails with cursor-based pagination + filters. */
list(query?: ListEmailsQuery): Promise;
- /** Fetch a single email and its delivery events. */
- get(id: string): Promise;
- /** Cancel a scheduled (PENDING) email before it fires. */
- cancelSchedule(id: string): Promise;
+ /**
+ * Fetch a single email together with its DELIVERY history, oldest first.
+ *
+ * `events` here is the delivery timeline behind `status` — not the custom
+ * events recorded with `events.record`, which are read from `events.list`.
+ * Before 1.1 this operation answered the wrong relation and published the
+ * message's dedup and idempotency ledger keys along with it.
+ */
+ get(id: string): Promise;
+ /**
+ * Cancel a scheduled (PENDING) email before it fires.
+ *
+ * Resolves the email itself, not an empty acknowledgement: the contract has
+ * always published `EmailResponse` here, and the caller wants the row's new
+ * status more than it wants a `{ success: true }` it already inferred from the
+ * absence of an exception.
+ */
+ cancelSchedule(id: string): Promise;
}
/**
@@ -10360,8 +17655,14 @@ declare class EventsResource {
}
/**
- * Subscription management for a mailing list, on the legacy `/api/*` surface
- * (envelope responses, camelCase — the SDK unwraps to `data`).
+ * Subscriber lists, on both surfaces.
+ *
+ * {@link subscribe} and {@link unsubscribe} speak the legacy `/api/*` dialect
+ * (camelCase inside a `{ success, data }` envelope the SDK unwraps) and accept
+ * SENDING_ONLY keys. The `V1`-suffixed methods manage the lists themselves on
+ * `/api/v1`: bare snake_case bodies and RFC 9457 problem documents. Both
+ * dialects describe the same lists, so the suffix is there to keep a call site
+ * from confusing one for the other.
*/
declare class ListsResource {
private readonly client;
@@ -10373,8 +17674,8 @@ declare class ListsResource {
* **Double opt-in.** When the list has `doubleOptIn` enabled the membership
* is created as `PENDING` and the result carries a `confirmToken`. Sendly
* does **not** send the confirmation email — your application must deliver
- * `/api/lists/confirm?token=` to the contact itself. The token
- * is valid for 24 hours.
+ * `/api/lists/confirm-subscription?token=` to the contact
+ * itself. The token is valid for 24 hours.
*
* **Re-subscribing after an opt-out.** If the email already holds an
* `UNSUBSCRIBED` membership on this list, the call fails with
@@ -10390,22 +17691,70 @@ declare class ListsResource {
subscribe(id: string, body: ListSubscribeRequest): Promise;
/** Unsubscribe a contact from a list. Resolves the address that was removed. */
unsubscribe(id: string, body: ListUnsubscribeRequest): Promise;
+ /**
+ * List the project's subscriber lists on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold the
+ * arguments steady for the whole walk — changing them mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ listV1(query?: ListListsV1Query): Promise;
+ /** Iterate every list across pages, yielding one list at a time. */
+ listAllV1(query?: ListListsV1Query): AsyncGenerator;
+ /**
+ * Create a list. Only `name` is required; `double_opt_in` defaults to false.
+ *
+ * Turning double opt-in on does not make Sendly send anything — it only
+ * changes {@link subscribe} to create the membership as `PENDING` and hand
+ * back the `confirmToken` your application delivers.
+ */
+ createV1(body: CreateListV1Request): Promise;
+ /**
+ * Retrieve a single list. `member_count` counts memberships in *any* status,
+ * `PENDING` and `UNSUBSCRIBED` included, so it is not the size of the
+ * audience a campaign would reach.
+ */
+ getV1(id: string): Promise;
+ /**
+ * Patch a list's name, description, opt-in mode, confirmation template, or
+ * redirect URL. Only the fields you send are changed; `member_count` is
+ * derived and never accepted here.
+ */
+ updateV1(id: string, body: UpdateListV1Request): Promise;
+ /** Delete a list. Resolves `{ id, deleted }`. Removes the list, not its contacts. */
+ deleteV1(id: string): Promise;
+ /**
+ * Start a bulk address-validation run over the list's members.
+ *
+ * **Billed per address checked**, so starting a run over a large list costs
+ * real money every time — it is not a free refresh. Answers `202` with the
+ * run in `pending`; read its progress and counts back with
+ * `validation.getRun`.
+ */
+ startValidationRun(id: string): Promise;
}
+/** What {@link MailboxesResource.sendMessage} resolves once the message is submitted. */
+type MailboxMessageSubmitted = paths["/api/mailboxes/{id}/messages"]["post"]["responses"][201]["content"]["application/json"]["data"];
+/** What {@link MailboxesResource.draftMessage} resolves — suggested text, and `sent: false`. */
+type MailboxMessageDraft = paths["/api/mailboxes/{id}/drafts"]["post"]["responses"][200]["content"]["application/json"]["data"];
/**
- * Receiving mailboxes on the project's verified domains.
+ * Receiving mailboxes on the project's verified domains, plus the two
+ * composition operations an API key may drive.
*
- * READ ONLY, and deliberately so. Creating and deleting a mailbox, and minting
- * or revoking an app password, all resolve the acting project admin from the
- * session user; an API key carries no user, so those routes answer `401` to any
- * `sk_` key however broad its scopes. The contract records that — they publish
- * `SessionAuth` without `ApiKeyAuth` — and this SDK authenticates only with API
- * keys, so a `create`/`delete` here could never succeed. They are listed in the
- * contract suite's `NOT_SDK_CALLABLE` rather than shipped as methods that
- * always throw.
+ * MAILBOX LIFECYCLE is what stays out of reach: creating and deleting a
+ * mailbox, and minting or revoking an app password, all resolve the acting
+ * project admin from the session user; an API key carries no user, so those
+ * routes answer `401` to any `sk_` key however broad its scopes. The contract
+ * records that — they publish `SessionAuth` without `ApiKeyAuth` — and this SDK
+ * authenticates only with API keys, so a `create`/`delete` here could never
+ * succeed. They are listed in the contract suite's `NOT_SDK_CALLABLE` rather
+ * than shipped as methods that always throw.
*
- * The three reads below are a different case: their membership check is
- * conditional, so a key really can call them.
+ * Everything below is a different case — the reads' membership check is
+ * conditional, and {@link sendMessage} / {@link draftMessage} publish
+ * `ApiKeyAuth` outright — so a key really can call them.
*/
declare class MailboxesResource {
private readonly client;
@@ -10441,6 +17790,43 @@ declare class MailboxesResource {
* this can identify a credential without being able to reconstruct it.
*/
listAppPasswords(id: string): Promise;
+ /**
+ * SENDS a new message — real mail leaves the account, from the mailbox in the
+ * path, over its own domain, and the recipient can reply to it.
+ *
+ * There is no `from` field, on purpose: a route that sends under a customer's
+ * own identity must not take that identity as an argument. `body` is plain
+ * text and HTML is refused — Sendly renders the HTML part itself, escaping as
+ * it goes, so text becomes markup in exactly one place.
+ *
+ * Bcc recipients are delivered to but appear in no header, so the copy filed
+ * in the mailbox's Sent folder does not record them. The message is stored as
+ * a new conversation, and the reply threads onto it.
+ *
+ * Refusals worth handling by name: `422 RECIPIENT_SUPPRESSED` (a recipient is
+ * on the project's suppression list), `422 CONTENT_REFUSED` (the outbound
+ * scanner declined it), `503 CONTENT_SCAN_UNAVAILABLE` (no verdict yet for a
+ * young project — nothing was sent, retry shortly). A mailbox may send 60
+ * messages an hour here.
+ */
+ sendMessage(id: string, body: ComposeMailboxMessageRequest): Promise;
+ /**
+ * SENDS NOTHING — asks Sendly's assistant to write text for this mailbox and
+ * hands it back for you to review. The response always reports `sent: false`,
+ * and no argument changes that.
+ *
+ * `mode` picks the job: `draft` writes a new email from a brief, `rewrite`
+ * reworks text you already have, `subject` returns alternative subject lines
+ * in `subjects`. The mailbox is named only so the text can be written in that
+ * address's voice; no correspondence is read and nothing is stored.
+ *
+ * That is why this asks only for `mailboxes:read` while {@link sendMessage}
+ * needs `mailboxes:send` — a client that may draft is not thereby a client
+ * that may mail your customers. Everything you pass is treated strictly as
+ * data describing what to write, never as instructions to the model. Capped
+ * at 120 requests an hour per project; `502` means the model was unreachable.
+ */
+ draftMessage(id: string, body: DraftMailboxMessageRequest): Promise;
}
/**
@@ -10502,32 +17888,245 @@ declare class SegmentsResource {
listContactsAll(id: string, query?: ListSegmentContactsV1Query): AsyncGenerator;
}
+/**
+ * Snippets — reusable body fragments a template pulls in with `{{> name}}`.
+ *
+ * Legacy dialect: `{ success, data }` envelopes and camelCase fields. Gated by
+ * the same `templates:*` scopes as the templates that include them, because a
+ * snippet is part of a template body rather than a resource with an audience of
+ * its own.
+ */
+declare class SnippetsResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * Create a snippet. `name` is the literal identifier templates include with
+ * `{{> name}}` and is unique within the project, so a clash answers 409.
+ */
+ create(body: CreateSnippetRequest): Promise;
+ /** List snippets with cursor pagination (`limit`/`cursor`) + optional `search` over name and description. */
+ list(query?: ListSnippetsQuery): Promise;
+ /** Fetch a single snippet by id. */
+ get(id: string): Promise;
+ /** Patch an existing snippet. */
+ update(id: string, body: UpdateSnippetRequest): Promise;
+ /**
+ * Delete a snippet. The API answers 200 with `{ success, data: { id } }`; the
+ * SDK resolves void. Templates that still include it keep rendering — an
+ * absent snippet renders as an empty string, like an absent variable.
+ */
+ delete(id: string): Promise;
+}
+
+/**
+ * The project suppression list — the addresses no send may reach — in both
+ * dialects.
+ *
+ * The unsuffixed methods speak legacy `/api/suppression` (singular path,
+ * `{ success, data }` envelopes); the `V1` methods speak `/api/v1/suppressions`
+ * (plural path, bare bodies, RFC 9457 problem documents). Both answer the same
+ * question, so the suffix is what stops a call site from reaching for one and
+ * reading the other's shape.
+ */
declare class SuppressionResource {
private readonly client;
constructor(client: Sendly);
/** Add an email to the project suppression list. */
add(body: AddSuppressionRequest): Promise;
- /** List suppressions with optional reason filter + cursor pagination. */
+ /**
+ * List suppressions with optional reason filter + cursor pagination.
+ *
+ * Alone among the legacy reads, this route answers no `{ success, data }`
+ * envelope: the page IS the body, `{ items, nextCursor }`, so nothing is
+ * unwrapped. Each record carries `scope` — `PROJECT` for every record this
+ * API creates or returns today.
+ */
list(query?: ListSuppressionsQuery): Promise;
/** Check whether a given email is suppressed. */
get(email: string): Promise;
/** Remove an email from the suppression list. Returns 204. */
remove(email: string): Promise;
+ /**
+ * List suppressed addresses, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold `reason`
+ * steady for the whole walk — changing it mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ listV1(query?: ListSuppressionsV1Query): Promise;
+ /** Iterate every suppressed address across pages, yielding one record at a time. */
+ listAllV1(query?: ListSuppressionsV1Query): AsyncGenerator;
+ /**
+ * Suppress an address, so no further send reaches it.
+ *
+ * Idempotent: an already-suppressed address answers `201` with the existing
+ * record, and the first `reason` wins — a later manual entry must not
+ * overwrite what an SES bounce recorded. `source` is not accepted in the
+ * body; it is derived from the credential, so a record's provenance cannot be
+ * dressed up as a deliverability fact.
+ */
+ createV1(body: CreateSuppressionV1Request): Promise;
+ /**
+ * Retrieve the suppression record for one address.
+ *
+ * The answer is definite either way: `200` means suppressed and says why,
+ * `404 resource_not_found` means the address is not on the list. A `200` may
+ * also come from a platform-wide block recorded outside this project.
+ */
+ getV1(email: string): Promise;
+ /**
+ * Un-suppress an address: mail can flow to it again. Resolves
+ * `{ email, deleted }`.
+ *
+ * This is the one call on this surface that can put mail back into an inbox
+ * that asked you to stop. It does NOT clear AWS SES's own account-level
+ * suppression list, so an address SES suppressed after a hard bounce stays
+ * undeliverable through SES even once this record is gone. Idempotent: an
+ * address that was never suppressed answers `200` too.
+ */
+ deleteV1(email: string): Promise;
}
+/**
+ * Reusable email templates, in both dialects.
+ *
+ * The unsuffixed methods speak legacy `/api/templates` — `{ success, data }`
+ * envelopes and camelCase fields. The `V1` methods speak `/api/v1/templates` —
+ * bare bodies, snake_case fields and RFC 9457 problem documents. Both answer
+ * the same question, so the suffix is what stops a call site from reaching for
+ * one and reading the other's shape.
+ */
declare class TemplatesResource {
private readonly client;
constructor(client: Sendly);
/** Create a reusable email template. */
create(body: CreateTemplateRequest): Promise;
- /** List templates with cursor pagination (`limit`/`cursor`) + optional type filter. */
+ /** List templates with cursor pagination (`limit`/`cursor`) + optional `emailCategory` filter. */
list(query?: ListTemplatesQuery): Promise;
/** Fetch a single template by id. */
get(id: string): Promise;
- /** Patch an existing template. */
+ /**
+ * Patch an existing template.
+ *
+ * An update that changes the rendered content increments `currentVersion`;
+ * one that only renames leaves it alone. A campaign records the version it
+ * sent, so comparing the two is how a caller tells "the template changed
+ * since" from "the template was renamed".
+ */
update(id: string, body: UpdateTemplateRequest): Promise;
/** Delete a template. The API answers 200 with `{ success, data: { id } }` (409 if still referenced); the SDK resolves void. */
delete(id: string): Promise;
+ /**
+ * List templates, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. `search` here
+ * matches the name only — narrower than the dashboard's search, which also
+ * reads description and subject. Hold `search` and `email_category` steady
+ * for the whole walk; changing either mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ listV1(query?: ListTemplatesV1Query): Promise;
+ /** Iterate every template across pages, yielding one template at a time. */
+ listAllV1(query?: ListTemplatesV1Query): AsyncGenerator;
+ /**
+ * Create a template. `email_category` defaults to `MARKETING` server-side.
+ *
+ * The `from` domain must already be a verified sending identity — an
+ * unverified sender is refused with `403 forbidden` here rather than becoming
+ * a campaign that fails at send time.
+ */
+ createV1(body: CreateTemplateV1Request): Promise;
+ /** Retrieve a single template. */
+ getV1(id: string): Promise;
+ /**
+ * Patch a template. Only the fields you send are changed.
+ *
+ * Touching `subject`, `body`, `from`, `from_name` or `reply_to` snapshots the
+ * previous content into version history and increments `version`; touching
+ * only `name`, `description` or `email_category` does not, because neither is
+ * content a send would have rendered.
+ */
+ updateV1(id: string, body: UpdateTemplateV1Request): Promise;
+ /**
+ * Delete a template. Resolves `{ id, deleted }` — the legacy `delete` above
+ * discards that body, this one hands it back.
+ *
+ * A template a workflow step or an active campaign (DRAFT, SCHEDULED or
+ * SENDING) still points at is refused with `409 conflict`. Emails already
+ * sent from it are not erased.
+ */
+ deleteV1(id: string): Promise;
+}
+
+/**
+ * Topics on the `/api/v1` surface — the consent vocabulary a project mails
+ * against. A contact subscribes to a topic rather than to a campaign, so
+ * switching one off silences a whole audience.
+ *
+ * Responses are bare v1 bodies (no `{ success, data }` envelope) and errors are
+ * RFC 9457 problem documents.
+ */
+declare class TopicsResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * List topics, newest first.
+ *
+ * Archived topics are omitted unless `include_archived` asks for them. There
+ * is no delete — archiving is the retire button, because a topic is where
+ * people's answers are recorded. {@link listAll} drives the loop for you.
+ *
+ * Paginated on `limit` + `after`, like every other v1 collection.
+ */
+ list(query?: ListTopicsV1Query): Promise;
+ /**
+ * Iterate every topic across pages, yielding one topic at a time.
+ *
+ * This used to be written out by hand: the endpoint named its cursor `cursor`
+ * on both sides where every other v1 list takes `after` and answers
+ * `next_cursor`, so the shared walker sent a parameter the route ignored and
+ * read a field it never returned — which silently re-fetched page one until
+ * `has_more` happened to be false. The route speaks the one dialect now, so
+ * this delegates like every other collection.
+ */
+ listAll(query?: ListTopicsV1Query): AsyncGenerator;
+ /**
+ * Create a topic.
+ *
+ * `key` is the stable name every preference form and integration refers to,
+ * so it survives a rename of `name` and cannot be changed afterwards.
+ *
+ * `default_opt_in` decides what silence means for a contact who never
+ * answers: true for a topic introduced over a list that already consented to
+ * hear from you, false for anything a person has to ask for.
+ */
+ create(body: CreateTopicV1Request): Promise;
+ /** Retrieve a single topic. */
+ get(id: string): Promise;
+ /**
+ * Patch a topic. Only the fields you send are changed.
+ *
+ * `key` is not patchable, and `archived: true` stands in for the delete that
+ * does not exist: it drops the topic from the preference centre and from new
+ * sends while every opt-out recorded against it survives.
+ */
+ update(id: string, body: UpdateTopicV1Request): Promise;
+ /**
+ * Record what one contact wants on one topic. The two directions are not
+ * symmetric, on purpose.
+ *
+ * `subscribed: true` does NOT subscribe anybody: it parks the contact at
+ * `pending` and answers a `confirmation_url`, and nothing is mailed on this
+ * topic until someone opens that link. There is no parameter to skip it —
+ * a caller asserting a subscription is not evidence the mailbox holder
+ * agreed. Sendly does not send the confirmation email; you do, from your own
+ * verified domain.
+ *
+ * `subscribed: false` records the opt-out immediately.
+ */
+ setSubscription(id: string, body: SetTopicSubscriptionV1Request): Promise;
}
/**
@@ -10548,6 +18147,63 @@ declare class UsageResource {
get(): Promise;
}
+/**
+ * Email validation on the `/api/v1` surface — check addresses before you mail
+ * them, and read back what a bulk run found.
+ *
+ * Responses are bare v1 bodies (no `{ success, data }` envelope) and errors are
+ * RFC 9457 problem documents.
+ */
+declare class ValidationResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * Check a batch of addresses. **This is billed per address checked** — every
+ * entry in `emails` costs money, so looping this over a contact list is
+ * looping over your invoice. Validate a whole list with the background run
+ * (`lists.startValidationRun`) instead of paging it through here.
+ *
+ * At most 50 addresses per call. That ceiling is a latency bound, not a
+ * payload one: every distinct domain in the batch costs a DNS round trip.
+ *
+ * Branch on each result's `verdict`, never on the flags — `is_personal`
+ * (Gmail, Outlook) and `is_role_address` (`support@`) describe ordinary,
+ * deliverable addresses that real customers use. A verdict of `unknown` means
+ * DNS did not answer in time, so that address was NOT checked; it is a
+ * separate value from `undeliverable` on purpose, and deleting a contact on
+ * `unknown` deletes a live one over a network hiccup.
+ */
+ validateEmails(body: ValidateEmailsV1Request): Promise;
+ /**
+ * Retrieve a bulk validation run: how far it has got, and what it found.
+ *
+ * The other way a run starts is `lists.startValidationRun`, which validates
+ * every address on a list in the background and answers with the run this
+ * method polls. A run is finished when `status` is `completed` or `failed` —
+ * never when a percentage reaches 100, because there is deliberately no total
+ * to divide by: a list changes size while a run walks it.
+ */
+ getRun(id: string): Promise;
+ /**
+ * List one page of a run's verdicts. Filter with `verdict` — `undeliverable`
+ * is the page to read before acting on a run, and `unknown` is the one never
+ * to act on, since those addresses were not actually checked.
+ *
+ * Pages on `after` and answers `next_cursor`, like every other v1
+ * collection. {@link listResultsAll} drives that loop for you.
+ */
+ listResults(id: string, query?: ListValidationResultsV1Query): Promise;
+ /**
+ * Iterate every result across pages, yielding one address's verdict at a time.
+ *
+ * This was hand-rolled through 1.0, because the endpoint spoke `cursor` on
+ * both sides while the shared helper sends `after` and reads `next_cursor` —
+ * so routing it through the helper would have sent an ignored parameter and
+ * re-fetched page one forever. The route speaks the one dialect now.
+ */
+ listResultsAll(id: string, query?: ListValidationResultsV1Query): AsyncGenerator;
+}
+
declare class VerifyResource {
private readonly client;
constructor(client: Sendly);
@@ -10563,6 +18219,15 @@ type ListWebhookCallsQuery = {
limit?: number;
cursor?: string;
};
+/**
+ * Webhook endpoints, on both surfaces.
+ *
+ * The unsuffixed methods speak the legacy `/api/*` dialect — camelCase bodies
+ * inside a `{ success, data }` envelope the SDK unwraps. The `V1`-suffixed
+ * methods speak `/api/v1`: bare snake_case bodies, cursor pagination, and
+ * RFC 9457 problem documents on error. Both answer the same questions, so the
+ * suffix is there to keep a call site from confusing one for the other.
+ */
declare class WebhooksResource {
private readonly client;
constructor(client: Sendly);
@@ -10570,6 +18235,10 @@ declare class WebhooksResource {
* Create a new outbound webhook subscription. The response includes the
* signing secret — store it now, it is only returned in full at creation
* and rotation time.
+ *
+ * `data` holds the two separately: `data.webhook` is the endpoint and
+ * `data.secret` is the plaintext. The endpoint's own fields are NOT spread
+ * alongside the secret, so the id is `data.webhook.id`.
*/
create(body: CreateWebhookRequest): Promise;
/** List all webhooks for the project. */
@@ -10584,6 +18253,64 @@ declare class WebhooksResource {
rotateSecret(id: string): Promise;
/** List recent delivery attempts for a webhook. */
listCalls(id: string, query?: ListWebhookCallsQuery): Promise;
+ /**
+ * List webhook endpoints, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count.
+ * {@link listAllV1} drives the loop for you. Signing secrets are not on this
+ * response — see {@link rotateSecretV1} if you have lost one.
+ */
+ listV1(query?: ListWebhooksV1Query): Promise;
+ /** Iterate every webhook endpoint across pages, yielding one endpoint at a time. */
+ listAllV1(query?: ListWebhooksV1Query): AsyncGenerator;
+ /**
+ * Register an endpoint to receive HMAC-signed deliveries for the events named
+ * in `event_types`.
+ *
+ * Resolves `{ webhook, secret }`, and this is one of only two calls that ever
+ * carry the signing secret — {@link rotateSecretV1} is the other. It is shown
+ * exactly once: no read endpoint returns it, so store it now, because a
+ * secret you lose is replaced by rotating rather than recovered. Feed it to
+ * `verifySignature` to authenticate the deliveries that arrive at your
+ * endpoint.
+ */
+ createV1(body: CreateWebhookV1Request): Promise;
+ /** Retrieve a single webhook endpoint. The signing secret is not on this response. */
+ getV1(id: string): Promise;
+ /**
+ * Patch a webhook endpoint. Omitted fields are left alone.
+ *
+ * `event_types` REPLACES the stored subscription list rather than merging
+ * into it, so an event you omit is unsubscribed. Setting `status` back to
+ * `ACTIVE` from `DISABLED` also clears the consecutive-failure counter, so an
+ * auto-disabled endpoint gets a clean slate. The signing secret is untouched
+ * by an update, and is not on this response.
+ */
+ updateV1(id: string, body: UpdateWebhookV1Request): Promise;
+ /**
+ * Delete a webhook endpoint, and its delivery history with it — a delivery
+ * attempt is a fact about this endpoint and means nothing once the endpoint is
+ * gone. Resolves `{ id, deleted }`. Deliveries already in flight are not
+ * recalled, so the endpoint may still receive an event shortly after this.
+ */
+ deleteV1(id: string): Promise;
+ /**
+ * Mint a fresh signing secret for an endpoint.
+ *
+ * The new plaintext is returned exactly once, here — this and
+ * {@link createV1} are the only two responses that ever carry the secret, and
+ * no read endpoint hands it back, so store it now and give it to
+ * `verifySignature`. A secret you lose is replaced by rotating again rather
+ * than recovered.
+ *
+ * The outgoing secret is not cut off at once: it keeps verifying until
+ * `previous_secret_expires_at`, and every delivery inside that window carries
+ * BOTH signatures, so a verifier can be redeployed without dropping an event.
+ * Past that moment the old secret starts being rejected — as does the older of
+ * two secrets if you rotate twice inside the window, because only one previous
+ * secret is ever live. `url`, `event_types` and `status` are unchanged.
+ */
+ rotateSecretV1(id: string): Promise;
}
/**
@@ -10642,10 +18369,73 @@ declare class WorkflowsResource {
* `{ from }`; there is no 90-day ceiling here, unlike `analytics.*`.
*/
stats(id: string, query?: WorkflowStatsV1Query): Promise;
+ /**
+ * Every step in the workflow — including its `TRIGGER` entry node — plus the
+ * directed transitions between them.
+ *
+ * A step's `config` comes back exactly as stored, camelCase keys and all,
+ * rather than projected into the snake_case used elsewhere on v1: the same
+ * document is authored by the visual editor, and renaming its keys on the way
+ * out would silently drop any key this API does not know on the way back in.
+ *
+ * `version` is the workflow's version at the time of the read, so a different
+ * number on a later read means somebody edited the graph in between. This
+ * body is accepted verbatim by {@link replaceGraph} — read, edit one step,
+ * send it back.
+ */
+ getGraph(id: string): Promise;
+ /**
+ * Replace the whole graph in one transaction.
+ *
+ * A `PUT` and not a `PATCH`, and that is the point: a graph is nodes *plus*
+ * the edges between them, so a partial edit to a step list has no meaning
+ * without the transitions that reference it — half-applied, it would leave
+ * steps pointing at steps that no longer exist.
+ *
+ * Ids decide the outcome per step: one you send is kept and updated in place,
+ * a fresh uuid creates a step, and an id you omit deletes that step *and its
+ * run history*. Exactly one step must be a `TRIGGER`, every transition must
+ * name steps in the same document, and no step may point at itself.
+ *
+ * Refused with `409 conflict` while the workflow has running executions —
+ * those runs are standing on the steps being replaced. {@link pause} first.
+ */
+ replaceGraph(id: string, body: ReplaceWorkflowGraphV1Request): Promise;
+ /**
+ * Copy a workflow and its whole graph as a new workflow.
+ *
+ * The copy is always created disabled, whatever the original was: a clone
+ * exists to be reviewed, and one that started live would match the same
+ * trigger events as its original from the moment it appeared. Pass `{ name }`
+ * to name it; it otherwise becomes `Copy of `.
+ */
+ clone(id: string, body: CloneWorkflowV1Request): Promise;
+ /**
+ * Disable the workflow *and cancel every `RUNNING`/`WAITING` execution in it*,
+ * resolving `{ workflow, cancelled_executions }`.
+ *
+ * That is what separates this from `update(id, { enabled: false })`, which
+ * only stops new runs starting and leaves every in-flight contact walking the
+ * graph — the next delay still expires, the next email still sends.
+ *
+ * The cancellation is terminal: {@link resume} re-opens the workflow to new
+ * runs, it does not put the cancelled contacts back where they were.
+ */
+ pause(id: string): Promise;
+ /**
+ * Re-enable the workflow so its trigger matches again. `cancelled_executions`
+ * is always 0 here — resuming starts nothing and stops nothing.
+ *
+ * Refused with `422 validation_error` while any step is still unconfigured,
+ * the same rule `update(id, { enabled: true })` enforces: an enabled workflow
+ * accepts contacts immediately and would otherwise fail only once one reached
+ * the broken step.
+ */
+ resume(id: string): Promise;
}
/** Build-time package version (kept in sync with package.json). */
-declare const SDK_VERSION = "1.0.0";
+declare const SDK_VERSION = "1.1.0";
/** Default production API base. Override via `baseUrl` for staging or self-hosted deployments. */
declare const DEFAULT_BASE_URL = "https://api.sendly.now";
interface SendlyClientOptions {
@@ -10663,8 +18453,14 @@ interface SendlyClientOptions {
interface RequestOptions {
/** Path relative to baseUrl, must start with `/`. */
path: string;
- /** HTTP method. */
- method: "GET" | "POST" | "PATCH" | "DELETE";
+ /**
+ * HTTP method.
+ *
+ * `PUT` exists for exactly one operation — replacing a workflow graph — and the
+ * distinction is the point: a graph is replaced whole, never patched, because a
+ * partial edit to a node list has no meaning without the edges that reference it.
+ */
+ method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
/** Optional JSON body. Will be serialized + Content-Type set. */
body?: unknown;
/**
@@ -10699,6 +18495,8 @@ declare class Sendly {
readonly events: EventsResource;
readonly verify: VerifyResource;
readonly lists: ListsResource;
+ /** Reusable body fragments a template includes with `{{> name}}`. */
+ readonly snippets: SnippetsResource;
/** Receiving mailboxes. Reads only — the writes need a user, not an API key. */
readonly mailboxes: MailboxesResource;
/** Campaigns on the versioned `/api/v1` surface. */
@@ -10713,6 +18511,12 @@ declare class Sendly {
readonly usage: UsageResource;
/** The project this key belongs to, on the versioned `/api/v1` surface. */
readonly projects: ProjectsResource;
+ /** Consent topics and what each contact has said they want. */
+ readonly topics: TopicsResource;
+ /** Address validation — one batch, or a whole list. */
+ readonly validation: ValidationResource;
+ /** Why mail from your domains is or is not arriving. */
+ readonly deliverability: DeliverabilityResource;
private readonly apiKey;
private readonly baseUrl;
private readonly fetchImpl;
@@ -10940,4 +18744,4 @@ declare function verifySignature(payload: string | Buffer, signature: string, ti
*/
declare function constructEvent>(payload: string | Buffer, signature: string, timestamp: string, secret: string, options?: VerifySignatureOptions): T;
-export { type AddDomainRequest, type AddSuppressionRequest, type AnalyticsCampaignStatsV1, type AnalyticsCampaignsV1Query, AnalyticsResource, type AnalyticsTimeseriesV1, type AnalyticsTimeseriesV1Query, type AnalyticsTopCampaignsV1, type AnalyticsWindowV1, type AppPasswordRecord, type BatchEntryResult, type BatchSendRequest, type BatchSendResponse, type BulkCreateContactsRequest, type BulkDeleteContactsRequest, type CampaignDeletedV1, type CampaignListV1, type CampaignStatsV1, type CampaignV1, CampaignsResource, type ContactListResponse, type ContactRecord, ContactsResource, type CreateCampaignV1Request, type CreateContactRequest, type CreateSegmentV1Request, type CreateTemplateRequest, type CreateWebhookRequest, type CreateWorkflowV1Request, type CursorPage, type CursorPageQuery, DEFAULT_BASE_URL, DEFAULT_TOLERANCE_MS, type DomainListResponse, type DomainRecord, type DomainSetupSession, type DomainVerificationStatus, DomainsResource, type EmailGetResponse, type EmailListResponse, type EmailRecord, type EmailTestV1, type EmailV1, EmailsResource, type ErrorEnvelope, type EventListV1, type EventNamesV1, type EventStatsV1, type EventStatsV1Query, type EventV1, EventsResource, type IdResponse, type IdempotencyOptions, type ListCampaignsV1Query, type ListContactsQuery, type ListEmailsQuery, type ListEventsV1Query, type ListSegmentContactsV1Query, type ListSegmentsV1Query, type ListSubscribeData, type ListSubscribeRequest, type ListSubscribeResponse, type ListSuppressionsQuery, type ListTemplatesQuery, type ListTopCampaignsV1Query, type ListUnsubscribeData, type ListUnsubscribeRequest, type ListUnsubscribeResponse, type ListWebhookCallsQuery, type ListWorkflowExecutionsV1Query, type ListWorkflowsV1Query, ListsResource, type MailboxDetail, type MailboxRecord, MailboxesResource, type Problem, type ProblemDocument, type ProblemFieldError, type ProjectV1, ProjectsResource, type RecordEventV1Request, type RequestOptions, SDK_VERSION, type SegmentContactListV1, type SegmentContactV1, type SegmentDeletedV1, type SegmentListV1, type SegmentV1, SegmentsResource, type SendCampaignV1Request, type SendEmailData, type SendEmailRequest, type SendEmailResponse, type SendEmailV1Request, type SendTestEmailV1Request, Sendly, SendlyAuthenticationError, type SendlyClientOptions, SendlyConflictError, SendlyConnectionError, SendlyError, SendlyNotFoundError, SendlyPermissionError, SendlyRateLimitError, SendlyServerError, SendlyValidationError, type StartWorkflowExecutionV1Request, type SuccessEmpty, type SuppressionCheckResponse, type SuppressionListResponse, type SuppressionRecord, SuppressionResource, type TemplateListResponse, type TemplateRecord, TemplatesResource, type TrackEventData, type TrackEventRequest, type TrackEventResponse, type UpdateCampaignV1Request, type UpdateContactRequest, type UpdateSegmentV1Request, type UpdateTemplateRequest, type UpdateWebhookRequest, type UpdateWorkflowV1Request, UsageResource, type UsageV1, type VerifyEmailData, type VerifyEmailRequest, type VerifyEmailResponse, VerifyResource, type VerifySignatureOptions, type WebhookCall, type WebhookCallsListResponse, type WebhookCreateResponse, type WebhookGetResponse, type WebhookListResponse, type WebhookRecord, type WebhookRotateSecretResponse, WebhooksResource, type WorkflowDeletedV1, type WorkflowExecutionListV1, type WorkflowExecutionV1, type WorkflowListV1, type WorkflowStatsV1, type WorkflowStatsV1Query, type WorkflowV1, WorkflowsResource, asProblemDocument, type components, constructEvent, type operations, paginateCursor, type paths, verifySignature };
+export { type AddDomainRequest, type AddSuppressionRequest, type AnalyticsCampaignStatsV1, type AnalyticsCampaignsV1Query, AnalyticsResource, type AnalyticsTimeseriesV1, type AnalyticsTimeseriesV1Query, type AnalyticsTopCampaignsV1, type AnalyticsWindowV1, type AppPasswordRecord, type AssignDomainStreamRequest, type BatchEntryResult, type BatchSendRequest, type BatchSendResponse, type BulkCreateContactsRequest, type BulkDeleteContactsRequest, type CampaignDeletedV1, type CampaignFailureListV1, type CampaignFailureV1, type CampaignListV1, type CampaignRetryFailedV1, type CampaignStatsV1, type CampaignV1, CampaignsResource, type CloneWorkflowV1Request, type ComposeMailboxMessageRequest, type ContactDeletedV1, type ContactListResponse, type ContactListV1, type ContactRecord, type ContactTopicPreferencesV1, type ContactV1, ContactsResource, type CreateCampaignV1Request, type CreateContactRequest, type CreateContactV1Request, type CreateDomainV1Request, type CreateListV1Request, type CreateSegmentV1Request, type CreateSnippetRequest, type CreateSuppressionV1Request, type CreateTemplateRequest, type CreateTemplateV1Request, type CreateTopicV1Request, type CreateWebhookRequest, type CreateWebhookV1Request, type CreateWorkflowV1Request, type CursorPage, type CursorPageQuery, DEFAULT_BASE_URL, DEFAULT_TOLERANCE_MS, type DeliverabilityDiagnosisV1, type DeliverabilityFindingSeverityV1, type DeliverabilityFindingV1, type DeliverabilityIdentityV1, type DeliverabilityRecentDeliveryV1, DeliverabilityResource, type DeliverabilitySuppressionV1, type DiagnoseDeliverabilityV1Query, type DmarcReportListV1, type DmarcReportV1, type DomainDeletedV1, type DomainListResponse, type DomainListV1, type DomainRecord, type DomainSetupSession, type DomainV1, type DomainVerificationStatus, DomainsResource, type DraftMailboxMessageRequest, type EmailDetailResponse, type EmailEvent, type EmailListResponse, type EmailRecord, type EmailResponse, type EmailTestV1, type EmailV1, type EmailValidationBatchV1, type EmailValidationResultListV1, type EmailValidationResultV1, type EmailValidationRunV1, type EmailValidationV1, type EmailValidationVerdictV1, type EmailWithEvents, EmailsResource, type ErrorEnvelope, type EventListV1, type EventNamesV1, type EventStatsV1, type EventStatsV1Query, type EventV1, EventsResource, type IdResponse, type IdempotencyOptions, type ListCampaignFailuresV1Query, type ListCampaignsV1Query, type ListContactsQuery, type ListContactsV1Query, type ListDeletedV1, type ListDmarcReportsV1Query, type ListDomainsV1Query, type ListEmailsQuery, type ListEventsV1Query, type ListListV1, type ListListsV1Query, type ListRecipientDomainStatsV1Query, type ListSegmentContactsV1Query, type ListSegmentsV1Query, type ListSnippetsQuery, type ListSubscribeData, type ListSubscribeRequest, type ListSubscribeResponse, type ListSuppressionsQuery, type ListSuppressionsV1Query, type ListTemplatesQuery, type ListTemplatesV1Query, type ListTopCampaignsV1Query, type ListTopicsV1Query, type ListUnsubscribeData, type ListUnsubscribeRequest, type ListUnsubscribeResponse, type ListV1, type ListValidationResultsV1Query, type ListWebhookCallsQuery, type ListWebhooksV1Query, type ListWorkflowExecutionsV1Query, type ListWorkflowsV1Query, ListsResource, type MailboxDetail, type MailboxRecord, MailboxesResource, type Problem, type ProblemDocument, type ProblemFieldError, type ProjectV1, ProjectsResource, type RecipientDomainStatsListV1, type RecipientDomainStatsV1, type RecordEventV1Request, type ReplaceWorkflowGraphV1Request, type RequestOptions, SDK_VERSION, type SegmentContactListV1, type SegmentContactV1, type SegmentDeletedV1, type SegmentListV1, type SegmentV1, SegmentsResource, type SendCampaignV1Request, type SendEmailData, type SendEmailRequest, type SendEmailResponse, type SendEmailV1Request, type SendTestEmailV1Request, Sendly, SendlyAuthenticationError, type SendlyClientOptions, SendlyConflictError, SendlyConnectionError, SendlyError, SendlyNotFoundError, SendlyPermissionError, SendlyRateLimitError, SendlyServerError, SendlyValidationError, type SetTopicSubscriptionV1Request, type SnippetListResponse, type SnippetRecord, SnippetsResource, type StartWorkflowExecutionV1Request, type SuccessEmpty, type SuppressionCheckResponse, type SuppressionDeletedV1, type SuppressionListResponse, type SuppressionListV1, type SuppressionRecord, SuppressionResource, type SuppressionV1, type TemplateDeletedV1, type TemplateListResponse, type TemplateListV1, type TemplateRecord, type TemplateV1, TemplatesResource, type TopicListV1, type TopicSubscriptionStatusV1, type TopicSubscriptionV1, type TopicV1, TopicsResource, type TrackEventData, type TrackEventRequest, type TrackEventResponse, type UpdateCampaignV1Request, type UpdateContactRequest, type UpdateContactV1Request, type UpdateListV1Request, type UpdateSegmentV1Request, type UpdateSnippetRequest, type UpdateTemplateRequest, type UpdateTemplateV1Request, type UpdateTopicV1Request, type UpdateWebhookRequest, type UpdateWebhookV1Request, type UpdateWorkflowV1Request, UsageResource, type UsageV1, type ValidateEmailsV1Request, ValidationResource, type VerifyEmailData, type VerifyEmailRequest, type VerifyEmailResponse, VerifyResource, type VerifySignatureOptions, type WebhookCall, type WebhookCallsListResponse, type WebhookCreateResponse, type WebhookCreatedV1, type WebhookDeletedV1, type WebhookGetResponse, type WebhookListResponse, type WebhookListV1, type WebhookRecord, type WebhookRotateSecretResponse, type WebhookSecretRotatedV1, type WebhookV1, WebhooksResource, type WorkflowDeletedV1, type WorkflowExecutionListV1, type WorkflowExecutionV1, type WorkflowGraphV1, type WorkflowListV1, type WorkflowStateChangeV1, type WorkflowStatsV1, type WorkflowStatsV1Query, type WorkflowV1, WorkflowsResource, asProblemDocument, type components, constructEvent, type operations, paginateCursor, type paths, verifySignature };
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 4c7ad0b..688d9a7 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -164,7 +164,17 @@ interface paths {
delete: operations["deleteDomain"];
options?: never;
head?: never;
- patch?: never;
+ /**
+ * Assign a sending identity to a stream
+ * @description Assign this identity to transactional or marketing traffic, make it the project's default for that stream, or give it the from-address a send on that stream uses when it names none.
+ *
+ * Streams are enforced, not labelled: once an identity is assigned, a send of the other kind from it is refused with 403. That is what keeps a campaign's complaint rate off the identity your password resets go out on. An identity with no stream (`stream: null`, and the state of every domain added before this existed) carries both.
+ *
+ * At most one identity per (project, stream) is the default; setting `streamDefault` demotes whichever held it. `defaultFromAddress` has to be an address on this identity's own host — a default pointing anywhere else would go out unsigned by the name in the From header. Every field is optional and an omitted one is left alone.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
+ */
+ patch: operations["assignDomainStream"];
trace?: never;
};
"/api/domains/{id}/dodomain-session": {
@@ -282,7 +292,9 @@ interface paths {
};
/**
* Get a single email
- * @description Fetch one email along with its delivery events.
+ * @description Fetch one email together with its DELIVERY history — the transitions behind `status`, oldest first.
+ *
+ * `events` here is not the custom-event resource: the events a caller records with `POST /api/v1/events` are read from `GET /api/v1/events`, and never appear on this response.
*
* Requires the `emails:read` scope — View the emails you have sent and their delivery status.
*/
@@ -326,7 +338,7 @@ interface paths {
put?: never;
/**
* Subscribe a contact to a list
- * @description Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm?token=` to the contact itself.
+ * @description Add a contact to a list, creating the contact if it does not exist. When the list has `doubleOptIn` enabled the membership is created as `PENDING` and the response carries a `confirmToken` — Sendly does NOT send the confirmation email, so the caller must deliver `/api/lists/confirm-subscription?token=` to the contact itself.
*
* Accepts SENDING_ONLY (`pk_*`) keys so it can back a public subscribe form.
*
@@ -491,6 +503,73 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/mailboxes/{id}/drafts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Draft a message with AI
+ * @description Ask Sendly's assistant to write a message for a mailbox — a new email from a short brief, a rewrite of something you already have, or a set of subject lines.
+ *
+ * **It returns text and sends nothing.** The response always reports `sent: false`, and there is no argument that changes that. Putting a draft in someone's inbox is a separate operation (`sendMailboxMessage`) under a separate scope, so a client that may draft is not thereby a client that may mail your customers.
+ *
+ * That is also why this operation asks only for `mailboxes:read`: it names a mailbox so the draft can be written in that address's voice, reads no correspondence, and stores nothing.
+ *
+ * Everything you pass — the brief, the draft, the recipient context — is treated strictly as data describing what to write, never as instructions to the model.
+ *
+ * Drafting is capped at 120 requests per hour per project.
+ *
+ * Requires the `mailboxes:read` scope — View the mailboxes on your domains and their settings.
+ */
+ post: operations["draftMailboxMessage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/mailboxes/{id}/messages": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send a message from a mailbox
+ * @description Compose and send a NEW message from a hosted mailbox — a first email to someone, not a reply inside a thread that already exists. It goes out from the mailbox's own address, over its own domain, and the recipient can reply to it.
+ *
+ * **The sender is the mailbox in the path.** There is no `from` field: a route that sends under a customer's own identity must not take the identity as an argument.
+ *
+ * **The body is plain text.** Sendly renders the HTML part from it, escaping as it goes, so there is one place where text becomes markup and it is inside Sendly. HTML is not accepted.
+ *
+ * Bcc recipients are delivered to but appear in no header, which also means the copy filed in the mailbox's Sent folder does not record them.
+ *
+ * Refusals worth handling by name:
+ *
+ * - `422 RECIPIENT_SUPPRESSED` — one or more recipients are on this project's suppression list. The message names them; remove them or take them off the list.
+ * - `422 CONTENT_REFUSED` — the outbound content scanner refused the message.
+ * - `503 CONTENT_SCAN_UNAVAILABLE` — screening could not reach a verdict for a young project. Nothing was sent; retry shortly.
+ * - `429` — a mailbox may send 60 messages an hour through this endpoint.
+ *
+ * The message is stored as a new conversation on the mailbox, so the reply threads onto it.
+ *
+ * Requires the `mailboxes:send` scope — Write and send new email from your hosted mailboxes, as that address.
+ */
+ post: operations["sendMailboxMessage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/projects/{id}/api-keys": {
parameters: {
query?: never;
@@ -510,7 +589,7 @@ interface paths {
* Create an API key
* @description Mint a new API key on the project. The token is NOT returned — the response carries the key's metadata plus a one-time `revealUrl` that only a signed-in dashboard session can open.
*
- * **Scope attenuation:** a key created with a delegated credential (an OAuth token or another API key) may not carry a scope that credential does not itself hold. A request that asks for more is refused with `400 SCOPE_ESCALATION` rather than quietly narrowed, so the mistake is reported where it was made instead of surfacing later as an unexplained 403. Naming only `permission` counts as asking for every scope that permission implies.
+ * **Scope attenuation:** a key created with a delegated credential (an OAuth token or another API key) may not carry a scope that credential does not itself hold. A request that asks for more is refused with `400 SCOPE_ESCALATION` rather than quietly narrowed, so the mistake is reported where it was made instead of surfacing later as an unexplained 403. Naming only `legacyGrantPreset` counts as asking for every scope that preset implies.
*
* Requires the `api-keys:write` scope — Create, rotate, and revoke API keys — these keep working even after you disconnect this app.
*/
@@ -565,6 +644,64 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/snippets": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List snippets
+ * @description Cursor-paginated list of the project's reusable template fragments. `search` matches name and description.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["listSnippets"];
+ put?: never;
+ /**
+ * Create a snippet
+ * @description `name` is the literal identifier templates include with `{{> name}}`: it must start with a letter and contain only letters, digits, hyphen or underscore, and it is unique within the project.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ post: operations["createSnippet"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/snippets/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get a snippet
+ * @description Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["getSnippet"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a snippet
+ * @description Templates that still include the snippet keep rendering — an absent snippet renders as an empty string, exactly like an absent variable.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ delete: operations["deleteSnippet"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a snippet
+ * @description Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ patch: operations["updateSnippet"];
+ trace?: never;
+ };
"/api/suppression": {
parameters: {
query?: never;
@@ -895,6 +1032,32 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/campaigns/{id}/failures": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List a campaign's failed sends
+ * @description The recipients this campaign did not reach, read from its per-contact send ledger. The campaign counters say how many were sent; only this says WHO was dropped and why, which is what makes retrying a decision rather than a guess.
+ *
+ * `reason` comes from a fixed vocabulary, not from the underlying error text, so it is stable enough to branch on. It is `null` for rows recorded before reasons were captured.
+ *
+ * Cursor-paginated like every other v1 list. Unlike them it also returns `total`: the retry action operates on that number, and a page that can only say `has_more` cannot tell you whether 3 or 30,000 sends failed.
+ *
+ * Requires the `campaigns:read` scope — View your campaigns and their performance.
+ */
+ get: operations["v1ListCampaignFailures"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/campaigns/{id}/pause": {
parameters: {
query?: never;
@@ -939,6 +1102,32 @@ interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/campaigns/{id}/retry-failed": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Retry a campaign's failed sends
+ * @description Re-drive only the recipients whose send failed, through the same pipeline the campaign used. Nobody who was already mailed is mailed again: each ledger row is claimed before it is touched, and a row whose email was created before the failure was recorded is re-queued rather than re-sent.
+ *
+ * The retry runs in the background, so this returns as soon as it is queued, with the number of rows it was queued for. Takes no request body.
+ *
+ * Only a `SENT` campaign can be retried: a `SENDING` or `PAUSED` one still has its own send in progress against the same ledger, and a `CANCELLED` one was stopped deliberately.
+ *
+ * Requires the `campaigns:write` scope — Create, edit, and organize your campaigns.
+ */
+ post: operations["v1RetryCampaignFailures"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/campaigns/{id}/send": {
parameters: {
query?: never;
@@ -989,65 +1178,75 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/emails": {
+ "/api/v1/contacts": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- put?: never;
/**
- * Send a transactional email
- * @description Send one transactional email and receive its delivery status in the same response. Accepts a `template` id or an inline `subject` + `body`.
- *
- * This is the send to reach for when you need to know what happened. The legacy `POST /api/emails` answers with row ids and no status, so telling an accepted send from a refused one costs a second request; here the receipt carries `status`, and `from` reports the sender actually used — which is worth reading, since a template may have supplied it.
- *
- * Exactly ONE recipient. A single receipt cannot describe a fan-out, so `to` takes one address: use `cc`/`bcc` to copy others on the same message, and `POST /api/emails/batch` to send different ones.
+ * List contacts
+ * @description Cursor-paginated list of contacts, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
*
- * `202 Accepted` is the success answer, and `PENDING` the usual `status`: the message is queued for the sending pipeline, not yet handed to the provider. Later states (`DELIVERED`, `BOUNCED`, …) arrive by webhook.
+ * A cursor is bound to the filters that minted it. Changing `search` or `subscribed` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
*
- * An optional `Idempotency-Key` header (1–255 chars, 24h TTL) makes a retry safe: the first request wins and a retry carrying the same key AND body replays its receipt. Reusing a key with a different body answers 422 rather than serving another request's result.
+ * Requires the `contacts:read` scope — View your contacts and their custom fields.
+ */
+ get: operations["v1ListContacts"];
+ put?: never;
+ /**
+ * Create a contact
+ * @description Create a contact. The address is unique per project, so creating one that already exists answers `409 conflict` rather than updating the existing row — there is no upsert on this surface, because a silent update is not what a caller who wrote `create` asked for.
*
- * Requires the `emails:send` scope — Send emails from your verified domains.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
*/
- post: operations["v1SendEmail"];
+ post: operations["v1CreateContact"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/emails/test": {
+ "/api/v1/contacts/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * Retrieve a contact
+ * @description Fetch one contact by id.
+ *
+ * Requires the `contacts:read` scope — View your contacts and their custom fields.
+ */
+ get: operations["v1GetContact"];
put?: never;
+ post?: never;
/**
- * Send a sandbox test email
- * @description Prove that sending works — before any domain, DNS record or verification exists.
+ * Delete a contact
+ * @description Delete the contact row. The emails already sent to that address are NOT erased — a send is a record of something that happened, and removing the recipient does not undo it. Erasing delivery history is a separate, irreversible operation that this surface deliberately does not expose.
*
- * The message is sent FROM this project's sandbox address (`sandbox_address` on `GET /api/v1/projects`) and can only reach ONE recipient: the project owner's own verified account email, which is also what `to` defaults to. Naming any other recipient answers 403, and naming a `from` answers 422 — the sender is resolved server-side and a request that expects a different one is refused rather than quietly re-addressed.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
+ */
+ delete: operations["v1DeleteContact"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a contact
+ * @description Partial update. Omitted fields are left alone.
*
- * That restriction is the reason `emails:test` is a separate, non-sensitive scope: a call under it cannot put mail in a stranger's inbox, so it can sit in a default grant where `emails:send` may not. It is not a way to send real mail cheaply — use `POST /api/v1/emails` for that.
+ * `email` is NOT writable: an address is the contact's identity on this API, and rewriting it in place would silently change what every earlier send was addressed to. Create the new address instead.
*
- * Sandbox sends are capped per project per day, and the response's `sandbox: true` marks the receipt so a relayed summary cannot pass a test send off as a real one.
+ * `custom_fields` REPLACES the stored object rather than merging into it, so a key you omit is gone. Read the contact first if you mean to change one key and keep the rest.
*
- * Requires the `emails:test` scope — Send test emails to your own address from the Sendly sandbox.
+ * Requires the `contacts:write` scope — Create, update, and delete your contacts.
*/
- post: operations["v1SendTestEmail"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
+ patch: operations["v1UpdateContact"];
trace?: never;
};
- "/api/v1/events": {
+ "/api/v1/contacts/{id}/topics": {
parameters: {
query?: never;
header?: never;
@@ -1055,37 +1254,25 @@ interface paths {
cookie?: never;
};
/**
- * List events
- * @description Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.
- *
- * A cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.
- *
- * Requires the `events:read` scope — View the custom events your application has recorded.
- */
- get: operations["v1ListEvents"];
- put?: never;
- /**
- * Record an event
- * @description Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.
- *
- * `contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.
+ * Get a contact's topic preferences
+ * @description Everything this contact has said they want, as the send path reads it.
*
- * Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.
- *
- * This endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.
+ * `subscribed` on each topic is the EFFECTIVE answer: a contact who has never answered has no row at all, and the topic's `default_opt_in` decides what that silence means. It is folded in here so no caller has to reimplement the rule.
*
- * Sending-only (`pk_*`) keys cannot record events — they hold the send capability and nothing else.
+ * The top-level `subscribed` is the global marketing opt-out, and it OUTRANKS every topic. A caller reading only the topic list would conclude somebody is reachable when they are not.
*
- * Requires the `events:write` scope — Record custom events for your contacts.
+ * Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
*/
- post: operations["v1TrackEvent"];
+ get: operations["v1GetContactTopicPreferences"];
+ put?: never;
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/events/names": {
+ "/api/v1/deliverability/diagnose": {
parameters: {
query?: never;
header?: never;
@@ -1093,12 +1280,16 @@ interface paths {
cookie?: never;
};
/**
- * List event names
- * @description Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.
+ * Diagnose why mail from a domain is not arriving
+ * @description Reads the sending domain's DNS identity, the project's delivery outcomes over the last `window_days`, and — when an `address` is given — that recipient's suppression state, then publishes `findings`: what is actually wrong, worst first, each with a stable `code` and the fix. Branch on `code`, never on the prose.
*
- * Requires the `events:read` scope — View the custom events your application has recorded.
+ * Everything here is read from data the platform already holds. The DNS statuses are the cached results of the verification refresh job, NOT a live lookup — `identity.last_checked_at` says when they were filled, and a domain that has never been checked reports nulls with a `dns_never_checked` finding rather than failures.
+ *
+ * `recent_delivery` is PROJECT-WIDE, not per-domain, because an email row records no sending domain; the field says so in its own `scope`. Rates are suppressed as meaningless below 20 sends in the window.
+ *
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1ListEventNames"];
+ get: operations["v1DiagnoseDeliverability"];
put?: never;
post?: never;
delete?: never;
@@ -1107,7 +1298,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/events/stats": {
+ "/api/v1/deliverability/dmarc": {
parameters: {
query?: never;
header?: never;
@@ -1115,14 +1306,18 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve event counts
- * @description Per-name event counts over a bounded window, most frequent first.
+ * DMARC aggregate reports for your domains
+ * @description DMARC aggregate (RUA) reports receiving providers have sent about your verified domains, newest reporting window first.
*
- * The window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.
+ * The only signal on this surface that does not come from us. A report is a receiver saying what it saw arrive claiming to be your domain, from every source — which is how a sender finds out both that their own alignment is broken and that somebody else is sending as them.
*
- * Requires the `events:read` scope — View the custom events your application has recorded.
+ * `pass_count` counts DMARC ALIGNMENT from `policy_evaluated`, not raw authentication results: a message can pass SPF for a domain that is not the one in its From header, and that is precisely the case DMARC exists to catch.
+ *
+ * Only reports about a domain registered in this project are stored, so a report about a domain you have not added will not appear here.
+ *
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1GetEventStats"];
+ get: operations["v1ListDmarcReports"];
put?: never;
post?: never;
delete?: never;
@@ -1131,7 +1326,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/projects": {
+ "/api/v1/deliverability/domains": {
parameters: {
query?: never;
header?: never;
@@ -1139,16 +1334,16 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve the authenticated project
- * @description The project the presented credential is scoped to — resolved from the API key, or from the `x-project-id` header for a session or delegated token. Singular despite the plural path, like `/api/v1/usage`: every v1 operation acts on exactly one project.
+ * Delivery outcomes per recipient domain
+ * @description Sent, delivered, bounced, complained and opened counts split by the RECIPIENT's domain and by UTC day, newest day first.
*
- * `sandbox_address` is this project's quick-start sender. It works with no domain setup, but only to the project owner's own verified address and under a daily cap — it is how you prove sending works end to end before any DNS exists. It is null when no handle can be derived (a project with no owner).
+ * This is the axis `diagnose` cannot report: its `recent_delivery` is project-wide, because an email row records no sending domain. A project-wide bounce rate hides the case that matters most — one recipient domain refusing almost everything while the rest is healthy.
*
- * To enumerate every project you belong to — a different question, and not project-scoped — use `GET /api/users/me/projects`.
+ * The counts are maintained by an hourly job over a rolling 30-day window, NOT computed on request; `computed_at` on each row says when it was last rebuilt. No rate is published: a rate over three sends is not information, and the counts let you apply your own threshold.
*
- * Requires the `projects:read` scope — View your projects and their settings.
+ * Requires the `deliverability:read` scope — Check why mail from one of your domains is not arriving.
*/
- get: operations["v1GetProject"];
+ get: operations["v1ListRecipientDomainStats"];
put?: never;
post?: never;
delete?: never;
@@ -1157,7 +1352,7 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/segments": {
+ "/api/v1/domains": {
parameters: {
query?: never;
header?: never;
@@ -1165,31 +1360,35 @@ interface paths {
cookie?: never;
};
/**
- * List segments
- * @description Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ * List sending domains
+ * @description Cursor-paginated list of sending domains, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
*
- * `member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.
+ * `verified` is SES's verdict on the identity and is the field that decides whether mail can leave from this domain. `dkim_verified` is a separate fact — what the DNS health refresh last read for the DKIM records — and the two disagree while a re-check is in flight, so do not treat either as a spelling of the other.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * Requires the `domains:read` scope — View your sending domains and their verification status.
*/
- get: operations["v1ListSegments"];
+ get: operations["v1ListDomains"];
put?: never;
/**
- * Create a segment
- * @description Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.
+ * Add a sending domain
+ * @description Register a domain and start SES DKIM verification. The response carries the identity as created — `verified` is false, because nothing is verified until the DKIM records are published in the domain's DNS and SES resolves them. Poll `/api/v1/domains/{id}/verify` after publishing them.
*
- * A `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.
+ * `region` pins the SES region. The first domain a project adds LOCKS the project to that region and every later domain must match it — a project split across regions would have its SES configuration diverge silently.
*
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ * `stream` assigns the identity to transactional or marketing traffic at creation; omit it to leave the identity serving both. `stream_default` makes it the project's default for that stream and therefore requires `stream` — sending it alone answers `422 validation_error` rather than being ignored.
+ *
+ * A host already registered — to this project or to another — answers `409 conflict` when the caller can already send from it and `403 project_access_denied` when it belongs elsewhere. A subdomain whose registrable root is held by a suspended project is refused the same way.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- post: operations["v1CreateSegment"];
+ post: operations["v1CreateDomain"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/segments/{id}": {
+ "/api/v1/domains/{id}": {
parameters: {
query?: never;
header?: never;
@@ -1197,118 +1396,113 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve a segment
- * @description Fetch one segment, including its saved `condition` and materialized `member_count`.
+ * Retrieve a sending domain
+ * @description Fetch one sending domain by id.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * Requires the `domains:read` scope — View your sending domains and their verification status.
*/
- get: operations["v1GetSegment"];
+ get: operations["v1GetDomain"];
put?: never;
post?: never;
/**
- * Delete a segment
- * @description Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.
+ * Remove a sending domain
+ * @description Remove the domain from the project. Refused with `409 conflict` while a template, workflow step or active campaign still sends from an address on this host — repoint those first, or their sends would start failing at SES with nothing here explaining why.
*
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ * The underlying SES identity is dropped too, unless another project still holds the same host. Its DKIM keys go with it, so re-adding the domain later mints new records that have to be published again.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- delete: operations["v1DeleteSegment"];
+ delete: operations["v1DeleteDomain"];
options?: never;
head?: never;
- /**
- * Update a segment
- * @description Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.
- *
- * `type` is not accepted here — see the create operation.
- *
- * Requires the `segments:write` scope — Create, edit, and delete your segments.
- */
- patch: operations["v1UpdateSegment"];
+ patch?: never;
trace?: never;
};
- "/api/v1/segments/{id}/contacts": {
+ "/api/v1/domains/{id}/verify": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * List the contacts in a segment
- * @description Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.
+ * Refresh a sending domain's verification state
+ * @description Re-read this domain's state from SES and DNS and return the refreshed document.
*
- * Cursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.
+ * This does NOT perform verification. Verification happens in the domain's own DNS, when its owner publishes the DKIM records SES minted at creation; Amazon decides when they resolve. What this call does is ask SES what it currently sees, re-check SPF and DMARC, and persist the answer — so a caller polling after a DNS change learns the outcome without waiting for the periodic sweep. Calling it on a domain whose records are not published yet is not an error and does not make it verify any sooner.
*
- * Requires the `segments:read` scope — View your segments and who belongs to them.
+ * A POST rather than a GET because it writes: the refreshed state is persisted, and a verified/unverified transition notifies the project.
+ *
+ * Requires the `domains:write` scope — Add and remove sending domains, and trigger verification.
*/
- get: operations["v1ListSegmentContacts"];
- put?: never;
- post?: never;
+ post: operations["v1VerifyDomain"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/usage": {
+ "/api/v1/email-validations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * Retrieve current usage and limits
- * @description Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.
+ * Validate a batch of email addresses
+ * @description Check up to 50 addresses for whether they can receive mail, and for the signals that make one worth mailing. Billed per address.
*
- * Every figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.
+ * The response publishes a `verdict` alongside the flags it was drawn from. Branch on the verdict: the two obvious readings of the flags are both wrong — a free-provider address (`is_personal`) and a role mailbox (`is_role_address`) are ordinary, deliverable addresses that real customers use, and refusing them would shrink a list for no reason. `is_disposable` is the only flag that lowers a verdict.
*
- * Two caveats worth reading before you alert on these numbers:
+ * `unknown` means DNS did not answer in time, so that address was NOT checked. It is a separate value from `undeliverable` on purpose — acting on the two together deletes live contacts over a network hiccup.
*
- * - The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.
- * - `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.
+ * The 50-address ceiling is a latency bound, not a payload one: every distinct DOMAIN in the batch costs a DNS round trip. To validate a whole list, use `POST /api/v1/lists/{id}/validation-runs`, which runs as a background job.
*
- * Requires the `usage:read` scope — View your usage totals and billing limits.
+ * Requires the `validation:write` scope — Check whether email addresses can receive mail — this is billed per address.
*/
- get: operations["v1GetUsage"];
- put?: never;
- post?: never;
+ post: operations["v1ValidateEmails"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows": {
+ "/api/v1/emails": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * List workflows
- * @description Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ * Send a transactional email
+ * @description Send one transactional email and receive its delivery status in the same response. Accepts a `template` id or an inline `subject` + `body`.
*
- * Unlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.
+ * This is the send to reach for when you need to know what happened. The legacy `POST /api/emails` answers with row ids and no status, so telling an accepted send from a refused one costs a second request; here the receipt carries `status`, and `from` reports the sender actually used — which is worth reading, since a template may have supplied it.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
- */
- get: operations["v1ListWorkflows"];
- put?: never;
- /**
- * Create a workflow
- * @description Creates an event-triggered workflow with a single trigger step. The rest of the graph (emails, delays, conditions) is built in the dashboard, so a workflow is created disabled and stays inert until it has steps to run.
+ * Exactly ONE recipient. A single receipt cannot describe a fan-out, so `to` takes one address: use `cc`/`bcc` to copy others on the same message, and `POST /api/emails/batch` to send different ones.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * `202 Accepted` is the success answer, and `PENDING` the usual `status`: the message is queued for the sending pipeline, not yet handed to the provider. Later states (`DELIVERED`, `BOUNCED`, …) arrive by webhook.
+ *
+ * An optional `Idempotency-Key` header (1–255 chars, 24h TTL) makes a retry safe: the first request wins and a retry carrying the same key AND body replays its receipt. Reusing a key with a different body answers 422 rather than serving another request's result.
+ *
+ * Requires the `emails:send` scope — Send emails from your verified domains.
*/
- post: operations["v1CreateWorkflow"];
+ post: operations["v1SendEmail"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/executions/{execution_id}/cancel": {
+ "/api/v1/emails/test": {
parameters: {
query?: never;
header?: never;
@@ -1318,19 +1512,25 @@ interface paths {
get?: never;
put?: never;
/**
- * Cancel a workflow execution
- * @description Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.
+ * Send a sandbox test email
+ * @description Prove that sending works — before any domain, DNS record or verification exists.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * The message is sent FROM this project's sandbox address (`sandbox_address` on `GET /api/v1/projects`) and can only reach ONE recipient: the project owner's own verified account email, which is also what `to` defaults to. Naming any other recipient answers 403, and naming a `from` answers 422 — the sender is resolved server-side and a request that expects a different one is refused rather than quietly re-addressed.
+ *
+ * That restriction is the reason `emails:test` is a separate, non-sensitive scope: a call under it cannot put mail in a stranger's inbox, so it can sit in a default grant where `emails:send` may not. It is not a way to send real mail cheaply — use `POST /api/v1/emails` for that.
+ *
+ * Sandbox sends are capped per project per day, and the response's `sandbox: true` marks the receipt so a relayed summary cannot pass a test send off as a real one.
+ *
+ * Requires the `emails:test` scope — Send test emails to your own address from the Sendly sandbox.
*/
- post: operations["v1CancelWorkflowExecution"];
+ post: operations["v1SendTestEmail"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}": {
+ "/api/v1/events": {
parameters: {
query?: never;
header?: never;
@@ -1338,35 +1538,37 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve a workflow
- * @description The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.
+ * List events
+ * @description Cursor-paginated list of recorded events, newest first. Filter by `event_name` to follow a single series.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * A cursor is bound to the filters it was issued under: pairing page 2's `next_cursor` with a different `event_name` answers 422 rather than returning a page that belongs to neither query.
+ *
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1GetWorkflow"];
+ get: operations["v1ListEvents"];
put?: never;
- post?: never;
/**
- * Delete a workflow
- * @description Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.
+ * Record an event
+ * @description Records a custom event, optionally attached to a contact. Events drive segment membership and workflow triggers, so a matching enabled workflow starts as a result of this call.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
- */
- delete: operations["v1DeleteWorkflow"];
- options?: never;
- head?: never;
- /**
- * Update a workflow
- * @description Sparse update — omitted fields are left unchanged.
+ * `contact_id` must already exist in this project — unlike `POST /api/track`, this endpoint never creates contacts. Omit it for a project-level event.
*
- * Two state rules apply: the trigger (`event_name`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ * Reserved system event names (`email.*`, `contact.subscribed`/`unsubscribed`, `segment.*.entry`/`.exit`) are rejected with 422: they are written by Sendly's own pipeline and accepting them from a caller would corrupt the series segments read.
*
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ * This endpoint does NOT require an `Idempotency-Key`. Events are append-only and the highest-volume write on the surface; a duplicate is a data-quality question for the caller, not a money-path hazard.
+ *
+ * Sending-only (`pk_*`) keys cannot record events — they hold the send capability and nothing else.
+ *
+ * Requires the `events:write` scope — Record custom events for your contacts.
*/
- patch: operations["v1UpdateWorkflow"];
+ post: operations["v1TrackEvent"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}/executions": {
+ "/api/v1/events/names": {
parameters: {
query?: never;
header?: never;
@@ -1374,29 +1576,21 @@ interface paths {
cookie?: never;
};
/**
- * List a workflow's executions
- * @description One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.
+ * List event names
+ * @description Every distinct event name in the project, most frequent first — the vocabulary a caller needs before filtering events or pointing a workflow trigger at one. Unpaginated: the set is bounded by what the integration emits, not by event volume.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1ListWorkflowExecutions"];
+ get: operations["v1ListEventNames"];
put?: never;
- /**
- * Start a workflow for a contact
- * @description Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.
- *
- * 409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.
- *
- * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
- */
- post: operations["v1StartWorkflowExecution"];
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workflows/{id}/stats": {
+ "/api/v1/events/stats": {
parameters: {
query?: never;
header?: never;
@@ -1404,12 +1598,14 @@ interface paths {
cookie?: never;
};
/**
- * Retrieve workflow statistics
- * @description Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.
+ * Retrieve event counts
+ * @description Per-name event counts over a bounded window, most frequent first.
*
- * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ * The window defaults to the last 30 days and never reaches further back than 90: this is a GROUP BY over the highest-volume table in the system, and an all-time answer is not one it can keep giving at scale. A wider request is narrowed rather than refused, and the `window` field states the range actually covered.
+ *
+ * Requires the `events:read` scope — View the custom events your application has recorded.
*/
- get: operations["v1GetWorkflowStats"];
+ get: operations["v1GetEventStats"];
put?: never;
post?: never;
delete?: never;
@@ -1418,27 +1614,41 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/verify": {
+ "/api/v1/lists": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * List subscriber lists
+ * @description Cursor-paginated list of subscriber lists, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `member_count` counts memberships in EVERY status — `PENDING` and `UNSUBSCRIBED` included — so it is the size of the membership table for this list, not the number of people currently subscribed. Read `GET /api/lists/{id}/members?status=CONFIRMED` when you want the latter.
+ *
+ * Requires the `lists:read` scope — View your subscriber lists and who is on them.
+ */
+ get: operations["v1ListLists"];
put?: never;
/**
- * Validate an email address
- * @description Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.
+ * Create a subscriber list
+ * @description Create an empty subscriber list. `member_count` on the response is 0 because the list has just been created — add contacts with `POST /api/lists/{id}/subscribe`.
+ *
+ * **`double_opt_in` does not make Sendly send anything.** With it on, `POST /api/lists/{id}/subscribe` creates the membership as `PENDING` and returns a `confirm_token`; delivering `/api/lists/confirm-subscription?token=` to the contact is YOUR job. A list that turns the flag on without sending that link collects pending memberships and confirms none of them.
+ *
+ * `description`, `confirmation_template_id` and `redirect_url` accept `null`, which means the same as omitting them: the field is left unset.
+ *
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
*/
- post: operations["verifyEmailAddress"];
+ post: operations["v1CreateList"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/webhooks": {
+ "/api/v1/lists/{id}": {
parameters: {
query?: never;
header?: never;
@@ -1446,57 +1656,63 @@ interface paths {
cookie?: never;
};
/**
- * List user webhooks
- * @description List all user-managed outbound webhooks for the auth'd project (secrets are not returned).
+ * Retrieve a subscriber list
+ * @description Fetch one list by id, with the same status-agnostic `member_count` the collection returns.
*
- * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ * Requires the `lists:read` scope — View your subscriber lists and who is on them.
*/
- get: operations["listWebhooks"];
+ get: operations["v1GetList"];
put?: never;
+ post?: never;
/**
- * Create a webhook
- * @description Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.
+ * Delete a subscriber list
+ * @description Delete the list and, by cascade, every membership on it. Those memberships are the consent record: an `UNSUBSCRIBED` row is the evidence that someone opted out, and it goes with the list — re-creating the list and re-importing the same addresses will not find their opt-outs waiting. The emails already sent are untouched.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
*/
- post: operations["createWebhook"];
- delete?: never;
+ delete: operations["v1DeleteList"];
options?: never;
head?: never;
- patch?: never;
+ /**
+ * Update a subscriber list
+ * @description Partial update. Omitted fields are left alone, and `null` for `description`, `confirmation_template_id` or `redirect_url` is treated the same as omitting them — this surface cannot clear a field back to empty yet.
+ *
+ * **`double_opt_in` does not make Sendly send anything.** With it on, `POST /api/lists/{id}/subscribe` creates the membership as `PENDING` and returns a `confirm_token`; delivering `/api/lists/confirm-subscription?token=` to the contact is YOUR job. A list that turns the flag on without sending that link collects pending memberships and confirms none of them.
+ *
+ * Turning `double_opt_in` on affects only memberships created afterwards. Existing `CONFIRMED` memberships stay confirmed: those contacts consented under the rule in force when they subscribed, and demoting them would revoke a consent record rather than collect one.
+ *
+ * Requires the `lists:write` scope — Create, rename, and delete your subscriber lists.
+ */
+ patch: operations["v1UpdateList"];
trace?: never;
};
- "/api/webhooks/{id}": {
+ "/api/v1/lists/{id}/validation-runs": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /**
- * Get a webhook
- * @description Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
- */
- get: operations["getWebhook"];
+ get?: never;
put?: never;
- post?: never;
/**
- * Delete a webhook
- * @description Hard-delete a webhook. Cascades to all WebhookCall rows.
+ * Validate every address on a list
+ * @description Start a background run over every contact on the list and answer immediately with the run. Poll `GET /api/v1/validation-runs/{id}` for progress and read the verdicts from its `/results` page.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * This VALIDATES and changes nothing: no membership is unsubscribed, no contact is deleted. What to do about an `undeliverable` address is your decision, and a run that acted on its own findings would be acting on a DNS lookup that can also answer `unknown`.
+ *
+ * A second run while one is already in flight for the same list is refused with 409 rather than queued: two runs would bill the same addresses twice.
+ *
+ * Requires the `validation:write` scope — Check whether email addresses can receive mail — this is billed per address.
*/
- delete: operations["deleteWebhook"];
+ post: operations["v1StartListValidationRun"];
+ delete?: never;
options?: never;
head?: never;
- /**
- * Update a webhook
- * @description Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
- */
- patch: operations["updateWebhook"];
+ patch?: never;
trace?: never;
};
- "/api/webhooks/{id}/calls": {
+ "/api/v1/projects": {
parameters: {
query?: never;
header?: never;
@@ -1504,12 +1720,16 @@ interface paths {
cookie?: never;
};
/**
- * List recent webhook calls
- * @description Cursor-paginated list of recent delivery attempts for a single webhook.
+ * Retrieve the authenticated project
+ * @description The project the presented credential is scoped to — resolved from the API key, or from the `x-project-id` header for a session or delegated token. Singular despite the plural path, like `/api/v1/usage`: every v1 operation acts on exactly one project.
*
- * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ * `sandbox_address` is this project's quick-start sender. It works with no domain setup, but only to the project owner's own verified address and under a daily cap — it is how you prove sending works end to end before any DNS exists. It is null when no handle can be derived (a project with no owner).
+ *
+ * To enumerate every project you belong to — a different question, and not project-scoped — use `GET /api/users/me/projects`.
+ *
+ * Requires the `projects:read` scope — View your projects and their settings.
*/
- get: operations["listWebhookCalls"];
+ get: operations["v1GetProject"];
put?: never;
post?: never;
delete?: never;
@@ -1518,2667 +1738,9623 @@ interface paths {
patch?: never;
trace?: never;
};
- "/api/webhooks/{id}/rotate-secret": {
+ "/api/v1/segments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ /**
+ * List segments
+ * @description Cursor-paginated list of segments, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `member_count` is materialized on each segment, so listing segments never fans out into one count per segment — it is refreshed as membership changes rather than computed on read.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1ListSegments"];
put?: never;
/**
- * Rotate the webhook signing secret
- * @description Generate a new shared secret. Returns the new plaintext secret exactly once.
+ * Create a segment
+ * @description Create a `DYNAMIC` segment (a saved `condition`, re-evaluated against contacts on every read) or a `STATIC` one (an explicitly managed membership list). `type` is fixed at creation — it decides how membership is computed, so it cannot be changed later.
*
- * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ * A `DYNAMIC` segment requires a `condition`, which is validated and evaluated during the request: the response's `member_count` tells you immediately how many contacts the filter actually matches.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
*/
- post: operations["rotateWebhookSecret"];
+ post: operations["v1CreateSegment"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
-}
-interface components {
- schemas: {
- /** @description Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region. */
- AddDomainBody: {
- domain: string;
- /** Format: uuid */
- projectId?: string;
- /**
- * @description Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region.
- * @enum {string}
- */
- region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ "/api/v1/segments/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/suppression — manually add an email to the suppression list. */
- AddSuppression: {
- /** Format: email */
- email: string;
- /**
- * @default MANUAL
- * @enum {string}
- */
- reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /**
+ * Retrieve a segment
+ * @description Fetch one segment, including its saved `condition` and materialized `member_count`.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1GetSegment"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a segment
+ * @description Delete a segment. Refused with 409 while any `DRAFT`, `SCHEDULED`, or `SENDING` campaign still targets it — deleting it would leave those campaigns pointing at an audience that no longer exists, and the failure would surface at send time instead of here. Remove the segment from those campaigns first.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ */
+ delete: operations["v1DeleteSegment"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a segment
+ * @description Partial update: an omitted field is left untouched. Changing a `DYNAMIC` segment's `condition` recomputes `member_count` in the same call, so the returned object never states a size that belongs to the previous filter. `condition` is ignored on a `STATIC` segment, whose membership is the explicit list.
+ *
+ * `type` is not accepted here — see the create operation.
+ *
+ * Requires the `segments:write` scope — Create, edit, and delete your segments.
+ */
+ patch: operations["v1UpdateSegment"];
+ trace?: never;
+ };
+ "/api/v1/segments/{id}/contacts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Campaign counters and engagement over the window. */
- AnalyticsCampaignStatsV1: {
- /** @description Campaigns in DRAFT or SCHEDULED. */
- active: number;
- average_click_rate: number;
- /** @description Percentage, one decimal place. */
- average_open_rate: number;
- completed: number;
- total: number;
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * List the contacts in a segment
+ * @description Cursor-paginated members of a segment. For a `STATIC` segment these are the rows of its membership list; for a `DYNAMIC` one the saved `condition` is evaluated against contacts as the page is read, so the result always reflects the contacts as they are now.
+ *
+ * Cursors from this endpoint are not interchangeable with cursors from other list endpoints — the ordering differs — and pairing one with the wrong endpoint answers 422 rather than silently paging a different set.
+ *
+ * Requires the `segments:read` scope — View your segments and who belongs to them.
+ */
+ get: operations["v1ListSegmentContacts"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/suppressions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Daily email counters across the window. Every day in range is present, zero-filled. */
- AnalyticsTimeseriesV1: {
- data: {
- bounces: number;
- clicks: number;
- /** Format: date-time */
- date: string;
- delivered: number;
- emails: number;
- opens: number;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * List suppressed addresses
+ * @description Cursor-paginated list of suppressed addresses, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * A cursor is bound to the filters that minted it. Changing `reason` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
+ *
+ * Requires the `suppression:read` scope — View the addresses on your suppression list.
+ */
+ get: operations["v1ListSuppressions"];
+ put?: never;
+ /**
+ * Suppress an address
+ * @description Add an address to this project's suppression list, so no further send reaches it.
+ *
+ * Idempotent: suppressing an already-suppressed address answers `201` with the EXISTING record rather than `409`. The first `reason` and `source` win, because a later manual entry must not overwrite what an SES bounce recorded.
+ *
+ * `source` is NOT accepted in the body — it is derived from the credential (`API` for an API key, `DASHBOARD` for a session), so a record's provenance cannot be dressed up as a deliverability fact.
+ *
+ * Requires the `suppression:write` scope — Add and remove addresses on your suppression list.
+ */
+ post: operations["v1CreateSuppression"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/suppressions/{email}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Sent campaigns ranked by open rate. */
- AnalyticsTopCampaignsV1: {
- data: {
- click_rate: number;
- clicked: number;
- /** Format: uuid */
- id: string;
- open_rate: number;
- opened: number;
- sent: number;
- subject: string;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
+ /**
+ * Check whether an address is suppressed
+ * @description Fetch the suppression record for one address. The path parameter is the address itself, URL-encoded.
+ *
+ * An address that is NOT suppressed answers `404 resource_not_found` — this path addresses the suppression record, and there is none. The answer is definite either way: `200` means suppressed and says why, `404` means not suppressed.
+ *
+ * A `200` may also come from a platform-wide block recorded outside this project, in which case the address is genuinely undeliverable for you even though you never suppressed it.
+ *
+ * Requires the `suppression:read` scope — View the addresses on your suppression list.
+ */
+ get: operations["v1GetSuppression"];
+ put?: never;
+ post?: never;
+ /**
+ * Remove an address from the suppression list
+ * @description Clear Sendly's suppression record for this address, so the send pipeline stops refusing it. This is the one operation on this surface that can put mail back into an inbox that asked you to stop, which is why `suppression:write` is a sensitive scope.
+ *
+ * It does NOT remove the address from AWS SES's own account-level suppression list. SES maintains that list independently of anything Sendly stores, so an address SES suppressed after a hard bounce stays undeliverable through SES even once this record is gone — removing it here is not a promise that the next send arrives.
+ *
+ * Idempotent: an address that was never suppressed answers `200` too, because "not on the list" is the state you asked for.
+ *
+ * Requires the `suppression:write` scope — Add and remove addresses on your suppression list.
+ */
+ delete: operations["v1DeleteSuppression"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/templates": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description The time range this response was computed over, after the 90-day clamp. */
- AnalyticsWindowV1: {
- /** Format: date-time */
- from: string;
- /** Format: date-time */
- to: string;
+ /**
+ * List templates
+ * @description Cursor-paginated list of templates, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * `search` is a case-insensitive substring match on the NAME only — narrower than the dashboard's search, which also reads description and subject, because a caller that asked for a name match should not be handed rows that merely mention the word in their body.
+ *
+ * A cursor is bound to the filters that minted it. Changing `search` or `email_category` while reusing a cursor answers `422 validation_error` rather than returning a page that belongs to neither query — drop the cursor and start again from the first page.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["v1ListTemplates"];
+ put?: never;
+ /**
+ * Create a template
+ * @description Create a template. The `from` domain must already be a verified sending identity for this project — an unverified sender answers `403 forbidden` here rather than becoming a campaign that fails at send time.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ post: operations["v1CreateTemplate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/templates/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
- ApiKey: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /** Format: uuid */
- domainId: string | null;
- /** Format: uuid */
- id: string;
- /** @description Last 4 characters of the token — the only fragment of the secret that survives creation. */
- lastFour: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- lastUsedAt: string | null;
- name: string;
- /** @enum {string} */
- permission: "FULL" | "SENDING_ONLY";
- /** Format: uuid */
- projectId: string;
- /**
- * Format: date-time
- * @description Set once the key is revoked. Revoked keys are NOT filtered out of list/get responses.
- */
- revokedAt: string | null;
- /** @description The explicit scope grant this key carries. Empty on a key minted before this column existed — that row's grant is derived from `permission` instead at request time. */
- scopes: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test")[];
+ /**
+ * Retrieve a template
+ * @description Fetch one template by id.
+ *
+ * Requires the `templates:read` scope — View your email templates.
+ */
+ get: operations["v1GetTemplate"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a template
+ * @description Delete the template. Answers `409 conflict` while a workflow step or an active campaign (DRAFT, SCHEDULED or SENDING) still points at it — removing it would leave those referring to content that no longer exists, and the failure would surface at send time instead of here. The emails already sent from this template are NOT erased.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ delete: operations["v1DeleteTemplate"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a template
+ * @description Partial update. Omitted fields are left alone.
+ *
+ * Changing `subject`, `body`, `from`, `from_name` or `reply_to` snapshots the previous content into the template's version history and increments `version`; changing only `name`, `description` or `email_category` does not, because neither is content a send would have rendered.
+ *
+ * A `from` supplied here is verified before anything is written, on the same terms as create.
+ *
+ * Requires the `templates:write` scope — Create, edit, and delete your email templates.
+ */
+ patch: operations["v1UpdateTemplate"];
+ trace?: never;
+ };
+ "/api/v1/topics": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Every API key on the project, including revoked ones — filter on `revokedAt` for live keys. */
- ApiKeyListResponse: {
- data: components["schemas"]["ApiKey"][];
- /** @enum {boolean} */
- success: true;
+ /**
+ * List topics
+ * @description The subjects this project mails about, cursor-paginated and newest first.
+ *
+ * Archived topics are omitted unless you ask for them with `include_archived`. Archiving is the retire button and there is no delete: a topic is where people's answers are recorded, so removing it would remove the choices they made.
+ *
+ * `subscribed_count` counts contacts who explicitly said yes. It reads low on a topic with `default_opt_in` true, and that is the honest number — it is how many people answered, not how many would currently receive the mail.
+ *
+ * Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
+ */
+ get: operations["v1ListTopics"];
+ put?: never;
+ /**
+ * Create a topic
+ * @description `key` is the stable, project-unique name every preference form and integration refers to, so it survives a rename of `name` and cannot be edited afterwards.
+ *
+ * `default_opt_in` is the field worth thinking about: it decides what SILENCE means for every contact who never answers. Leave it true for a topic introduced over a list you already have — those contacts consented to hear from you, and inventing an opt-out they never asked for would mute mail they expect. Set it false for anything a person has to ask for, and absence then means `not asked` rather than `no`.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ post: operations["v1CreateTopic"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/topics/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An IMAP/SMTP credential for one mailbox, described but never reproduced. */
- AppPassword: {
- /** Format: date-time */
- createdAt: string;
- /** Format: uuid */
- id: string;
- /** @description The last four characters of the secret — enough to tell two credentials apart, and nothing more. */
- lastFour: string;
- /**
- * Format: date-time
- * @description Null until a mail client has authenticated with it at least once.
- */
- lastUsedAt: string | null;
- /** @description What the credential is for, e.g. `Thunderbird on my laptop`. */
- name: string;
- /** @description Which protocols this password may authenticate. `imap` reads, `smtp` sends. */
- scopes: ("imap" | "smtp")[];
- };
- /** @description A newly created app password, handed over as a one-time link rather than as a secret. */
- AppPasswordReveal: {
- /** Format: uuid */
- id: string;
- /**
- * Format: date-time
- * @description When the link stops working. Five minutes after creation; the password itself does not expire.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A single-use link that shows the password once, in a browser. Opening it requires a signed-in Sendly session belonging to a project admin — the connection that created the password cannot open it, and the second attempt to open it fails whoever makes it.
- */
- revealUrl: string;
- };
- /** @description Per-row result in a batch send response. */
- BatchEntryResult: {
- data?: components["schemas"]["SendEmailData"];
- error?: {
- code: string;
- message: string;
- };
- index: number;
- /** @enum {string} */
- status: "ok" | "error";
+ /**
+ * Retrieve a topic
+ * @description Requires the `topics:read` scope — View the topics you mail about and who is subscribed to each.
+ */
+ get: operations["v1GetTopic"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Update a topic
+ * @description Rename it, re-describe it, flip `default_opt_in`, or archive it.
+ *
+ * `key` is absent from the body on purpose. Every stored preference and every integration refers to a topic by its key, so changing one would orphan them silently. There is no DELETE for the same reason — `archived: true` removes the topic from the preference centre and from new sends while every opt-out recorded against it survives.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ patch: operations["v1UpdateTopic"];
+ trace?: never;
+ };
+ "/api/v1/topics/{id}/subscriptions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Batch send wrapper. Up to 100 entries. */
- BatchSendBody: {
- emails: components["schemas"]["SendEmail"][];
+ get?: never;
+ put?: never;
+ /**
+ * Subscribe or unsubscribe a contact from a topic
+ * @description The two directions behave differently, and the asymmetry is deliberate: consent needs proof, withdrawal of consent does not.
+ *
+ * `subscribed: true` does NOT subscribe anyone. It parks the contact at `pending` and returns a `confirmation_url`; nothing is mailed on this topic until somebody opens it. There is no parameter to skip that step. A caller asserting a subscription is not evidence that the mailbox holder agreed — anyone can type an address into a form — and treating the assertion as consent is the exact failure double opt-in exists to prevent. Sendly does not send the confirmation email; you do, from your own verified domain, because it is your relationship with the contact and your sending reputation.
+ *
+ * `subscribed: false` records the opt-out immediately. Requiring somebody to confirm that they want to stop is a dark pattern, and it is also the fastest route to a spam report.
+ *
+ * Requires the `topics:write` scope — Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach.
+ */
+ post: operations["v1SetTopicSubscription"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/usage": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200. */
- BatchSendResponse: {
- data: components["schemas"]["BatchEntryResult"][];
- success: boolean;
+ /**
+ * Retrieve current usage and limits
+ * @description Email usage against the limits that are actually enforced: the current month's counts per source category, the monthly cap applied to their total, and today's sends against the trust-tier daily ceiling.
+ *
+ * Every figure is read from an enforcement path, so what this reports and what refuses a send cannot disagree. Correspondingly, nothing else is published — there is no billing period, invoice total or non-email meter here, because the platform meters none of those.
+ *
+ * Two caveats worth reading before you alert on these numbers:
+ *
+ * - The windows differ. The monthly counters roll over on the SERVER's calendar month; the daily counter buckets on the UTC date. The two therefore reset at different instants.
+ * - `monthly.limit` is null once a subscription makes sending metered rather than capped, and also when an operator has set per-category limits — in that case the caps live in `monthly.categories[*].limit`.
+ *
+ * Requires the `usage:read` scope — View your usage totals and billing limits.
+ */
+ get: operations["v1GetUsage"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/validation-runs/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description A campaign as exposed on the v1 API. */
- CampaignV1: {
- /** @enum {string} */
- audience_type: "ALL" | "FILTERED" | "SEGMENT";
- /** Format: date-time */
- created_at: string;
- /** Format: uuid */
- id: string;
- name: string;
- /** Format: date-time */
- scheduled_at: string | null;
- /** Format: date-time */
- sent_at: string | null;
- stats: {
- bounced: number;
- clicked: number;
- delivered: number;
- opened: number;
- sent: number;
- total_recipients: number;
- };
- /** @enum {string} */
- status: "DRAFT" | "SCHEDULED" | "SENDING" | "PAUSED" | "SENT" | "CANCELLED";
- subject: string;
+ /**
+ * Retrieve a validation run
+ * @description How far a run has got and what it found. The counters are advanced by each batch in the same step that commits the rows they count, so they are a ledger rather than a cached estimate and may be branched on.
+ *
+ * There is no total to divide by, deliberately: a list changes size while a run walks it, so a denominator captured when the run started would be wrong by the time you read it. A run is finished when `status` is `completed` or `failed`, never when a percentage reaches 100.
+ *
+ * Requires the `validation:read` scope — View your email validation runs and their results.
+ */
+ get: operations["v1GetValidationRun"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/validation-runs/{id}/results": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, and `audience_condition` is required when it is `FILTERED`. */
- CampaignV1Create: {
- audience_condition?: components["schemas"]["FilterConditionV1"];
- /**
- * @description `ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`.
- * @enum {string}
- */
- audience_type: "ALL" | "FILTERED" | "SEGMENT";
- body: string;
- description?: string;
- /**
- * Format: email
- * @description Sender address. Its domain must be verified for this project.
- */
- from: string;
- from_name?: string | null;
- name: string;
- /** Format: email */
- reply_to?: string | null;
- /** Format: uuid */
- segment_id?: string;
- subject: string;
- /**
- * @default MARKETING
- * @enum {string}
- */
- type: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ /**
+ * List a validation run's results
+ * @description One page of a run's verdicts, cursor-paginated. Filter with `verdict` — `undeliverable` is the page you want when you are about to act on the results.
+ *
+ * No total is reported: a run over a large list holds millions of rows, and counting them on every page read is the query this design exists to avoid. The run's own counters carry the numbers worth having.
+ *
+ * `contact_id` is the contact the address belonged to when it was checked, and it is null for a contact deleted since. The `email` is stored on the result rather than read through the contact, so a completed run reports what it actually checked even after the contact changed address.
+ *
+ * Requires the `validation:read` scope — View your email validation runs and their results.
+ */
+ get: operations["v1ListValidationRunResults"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Acknowledgement that a campaign was deleted. */
- CampaignV1Deleted: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ /**
+ * List webhooks
+ * @description Cursor-paginated list of webhook endpoints, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * Signing secrets are not on this response and cannot be read back — see `POST /api/v1/webhooks/{id}/rotate-secret` if you have lost one.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["v1ListWebhooks"];
+ put?: never;
+ /**
+ * Create a webhook
+ * @description Register an endpoint to receive HMAC-signed deliveries for the events named in `event_types`.
+ *
+ * The response carries the signing secret ONCE. It is shown here and by `POST /api/v1/webhooks/{id}/rotate-secret`, and by nothing else — no endpoint reads it back, so store it now. A secret you have lost is replaced by rotating, not recovered.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["v1CreateWebhook"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Cursor-paginated list of campaigns. */
- CampaignV1List: {
- data: components["schemas"]["CampaignV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ /**
+ * Retrieve a webhook
+ * @description Fetch one webhook endpoint by id. The signing secret is not part of this response.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["v1GetWebhook"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a webhook
+ * @description Remove the endpoint and its delivery history — a delivery attempt is a fact about this endpoint and has no meaning once the endpoint is gone. Deliveries already in flight are not recalled, so the endpoint may still receive an event shortly after this returns.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ delete: operations["v1DeleteWebhook"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a webhook
+ * @description Partial update. Omitted fields are left alone.
+ *
+ * `event_types` REPLACES the stored subscription list rather than merging into it, so an event you omit is unsubscribed. Setting `status` back to `ACTIVE` from `DISABLED` also clears the consecutive-failure counter, so an auto-disabled endpoint gets a clean slate.
+ *
+ * The signing secret is untouched by an update, and is not part of this response.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ patch: operations["v1UpdateWebhook"];
+ trace?: never;
+ };
+ "/api/v1/webhooks/{id}/rotate-secret": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/campaigns/{id}/send. */
- CampaignV1Send: {
- /**
- * Format: date-time
- * @description RFC 3339 timestamp, strictly in the future. A numeric UTC offset (`+02:00`) is accepted as well as `Z`. Omit to start sending immediately.
- */
- scheduled_for?: string;
+ get?: never;
+ put?: never;
+ /**
+ * Rotate a webhook signing secret
+ * @description Mint a fresh signing secret. The new plaintext is returned EXACTLY ONCE, here — no endpoint reads it back, so a secret you lose is replaced by rotating again rather than recovered.
+ *
+ * Rotation overlaps on purpose. The outgoing secret keeps verifying until `previous_secret_expires_at`, and every delivery inside that window carries BOTH signatures in the `webhook-signature` header — so you can deploy the new secret to your verifier whenever you like without dropping an in-flight event. Rotating twice inside the window discards the older secret: only one previous secret is ever live.
+ *
+ * `url`, `event_types` and `status` are unchanged.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["v1RotateWebhookSecret"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Materialized delivery and engagement counters for one campaign. */
- CampaignV1Stats: {
- bounce_rate: number;
- bounced: number;
- click_rate: number;
- clicked: number;
- delivered: number;
- delivery_rate: number;
- open_rate: number;
- opened: number;
- sent: number;
- total_recipients: number;
+ /**
+ * List workflows
+ * @description Cursor-paginated list of workflows, newest first. Pass the previous response's `next_cursor` as `after` to page forward; `has_more` is false and `next_cursor` is null on the last page.
+ *
+ * Unlike the legacy `/api/*` endpoints, v1 responses are the bare payload (no `{ success, data }` envelope) and errors are RFC 9457 problem documents.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1ListWorkflows"];
+ put?: never;
+ /**
+ * Create a workflow
+ * @description Creates a workflow with its single trigger step. `trigger_type` defaults to `EVENT`, which requires `event_name`; `SCHEDULE` takes `interval_ms` and `MANUAL` is started only by `POST /api/v1/workflows/{id}/executions`.
+ *
+ * Pass `sequence` to create the steps at the same time, as a LINEAR chain behind the trigger. Anything with a branch is `PUT /api/v1/workflows/{id}/graph`. Without a sequence the workflow holds only its trigger and stays inert until it has steps to run, which is why it is created disabled.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CreateWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/executions/{execution_id}/cancel": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for PATCH /api/v1/campaigns/{id}. All fields optional. */
- CampaignV1Update: {
- audience_condition?: components["schemas"]["FilterConditionV1"];
- /** @enum {string} */
- audience_type?: "ALL" | "FILTERED" | "SEGMENT";
- body?: string;
- description?: string;
- /**
- * Format: email
- * @description Sender address. Its domain must be verified for this project.
- */
- from?: string;
- from_name?: string | null;
- name?: string;
- /** Format: email */
- reply_to?: string | null;
- /** Format: uuid */
- segment_id?: string;
- subject?: string;
- /** @enum {string} */
- type?: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ get?: never;
+ put?: never;
+ /**
+ * Cancel a workflow execution
+ * @description Stops one run and stamps it `CANCELLED`. The execution stays queryable — cancelling is a state change, not a delete. Addressed by execution id alone, so a caller holding one from a list does not need to carry the workflow id with it.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CancelWorkflowExecution"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description A subscriber/contact within a project. */
- Contact: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- customFields?: {
- [key: string]: unknown;
- } | null;
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
- /** Format: uuid */
- projectId: string;
- subscribed: boolean;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
+ /**
+ * Retrieve a workflow
+ * @description The workflow itself — its trigger, re-entry policy and rate cap. The step graph is not part of the v1 contract.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflow"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a workflow
+ * @description Refused with 409 while executions are still running: deleting a workflow cascades its executions away, and a contact mid-journey disappearing is data loss the caller cannot detect afterwards. Disable the workflow or cancel its runs first.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ delete: operations["v1DeleteWorkflow"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a workflow
+ * @description Sparse update — omitted fields are left unchanged.
+ *
+ * Two state rules apply: the trigger (`trigger_type`/`event_name`/`interval_ms`) cannot be changed while the workflow has running executions (409), and `enabled: true` is refused while any step is still unconfigured (422), because an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ *
+ * `sequence` REPLACES every non-trigger step with a linear chain and is likewise refused while executions are running. Omit it to leave the graph untouched.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ patch: operations["v1UpdateWorkflow"];
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/clone": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Bulk create up to 1000 contacts. */
- ContactBulkCreateBody: {
- contacts: components["schemas"]["CreateContact"][];
+ get?: never;
+ put?: never;
+ /**
+ * Clone a workflow
+ * @description Copies a workflow and its whole graph as a new workflow. The copy is always created disabled, whatever the original was: a clone exists to be reviewed, and one that started live would match the same trigger events as its original from the moment it appeared.
+ *
+ * Server-side rather than a read-then-write, so the copy is taken from one consistent read of the source.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1CloneWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/executions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each). */
- ContactBulkDeleteBody: {
- emails?: string[];
- ids?: string[];
+ /**
+ * List a workflow's executions
+ * @description One row per contact-run, newest first, cursor-paginated on the execution's start time. Filter by `status` to find stuck (`WAITING`) or failed runs.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1ListWorkflowExecutions"];
+ put?: never;
+ /**
+ * Start a workflow for a contact
+ * @description Enters one contact into an enabled workflow. Step processing runs asynchronously, so a 201 means the run was claimed — not that it finished.
+ *
+ * 409 when the workflow's re-entry policy already accounts for this contact; 429 when the workflow's own `max_executions_per_hour` cap is reached.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1StartWorkflowExecution"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/graph": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Cursor-paginated list of contacts. */
- ContactListResponse: {
- data: {
- data: components["schemas"]["Contact"][];
- hasMore: boolean;
- /** @description Cursor for the next page, or null on the last page. */
- nextCursor: string | null;
- total: number;
- };
- /** @enum {boolean} */
- success: true;
+ /**
+ * Retrieve a workflow's step graph
+ * @description Every step in the workflow, including its `TRIGGER` entry node, and every directed transition between them. `version` is the workflow's version at the time of the read — a different number on a later read means somebody edited the graph in between.
+ *
+ * A step's `config` is returned exactly as stored, camelCase keys and all, rather than projected into the snake_case used everywhere else on this API. That is deliberate: the same document is authored by the visual editor, and renaming its keys on the way out would mean any key this API did not know about was silently dropped on the way back in.
+ *
+ * The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path: read a graph, change a step, send it back. Step and transition ids are yours to choose on a write, which is what makes that round trip a no-op rather than a rebuild.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflowGraph"];
+ /**
+ * Replace a workflow's step graph
+ * @description Replaces the whole graph in one transaction, because a graph is only meaningful as nodes PLUS the edges between them — an edit that could apply half of it would leave steps pointing at steps that no longer exist.
+ *
+ * A step whose id you send is kept and updated in place; a fresh uuid creates one; an id you omit deletes that step and its run history. Exactly one step must be a `TRIGGER`, every transition must name steps present in the same document, and a step may not point at itself.
+ *
+ * Refused with 409 while the workflow has running executions: those runs are standing on the steps being replaced. Pause the workflow (`POST /api/v1/workflows/{id}/pause`) first.
+ *
+ * The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path: read a graph, change a step, send it back. Step and transition ids are yours to choose on a write, which is what makes that round trip a no-op rather than a rebuild.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ put: operations["v1ReplaceWorkflowGraph"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/pause": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateApiKeyBody: {
- /** Format: uuid */
- domainId?: string | null;
- name: string;
- /** @enum {string} */
- permission?: "FULL" | "SENDING_ONLY";
- /** @description The explicit grant the new key will carry. Omitted ⇒ materialised from `permission`. A `SENDING_ONLY` key may carry only `emails:send`. */
- scopes?: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test")[];
+ get?: never;
+ put?: never;
+ /**
+ * Pause a workflow and cancel its running executions
+ * @description Disables the workflow and cancels every `RUNNING`/`WAITING` execution inside it.
+ *
+ * `PATCH { "enabled": false }` stops NEW runs starting and leaves every in-flight contact walking the graph — the next delay still expires, the next email still sends. Pausing does both, and reports how many runs it stopped.
+ *
+ * Cancelling is terminal: `resume` re-opens the workflow to new runs, it does not put the cancelled contacts back where they were.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1PauseWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/resume": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/mailboxes/:id/app-passwords. */
- CreateAppPassword: {
- name: string;
- /**
- * @default [
- * "imap",
- * "smtp"
- * ]
- */
- scopes: ("imap" | "smtp")[];
+ get?: never;
+ put?: never;
+ /**
+ * Resume a paused workflow
+ * @description Re-enables the workflow so its trigger matches again. `cancelled_executions` is always 0 here — resuming starts nothing and stops nothing.
+ *
+ * Refused with 422 while any step is still unconfigured, the same rule `PATCH { "enabled": true }` enforces: an enabled workflow accepts contacts immediately and would otherwise fail only once one reached the broken step.
+ *
+ * Requires the `workflows:write` scope — Create, edit, enable, and delete your automation workflows.
+ */
+ post: operations["v1ResumeWorkflow"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/workflows/{id}/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/contacts and /api/contacts/upsert. */
- CreateContact: {
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- customFields?: {
- [key: string]: unknown;
- };
- /** Format: email */
- email: string;
- /** @default true */
- subscribed: boolean;
+ /**
+ * Retrieve workflow statistics
+ * @description Execution counts by status, average completion time, the emails this workflow sent (with opens and clicks), and per-goal conversion counts. All-time by default — pass `from` to narrow it. Unlike `/api/v1/analytics/*` there is no 90-day ceiling here, because every aggregate is already confined to this one workflow.
+ *
+ * The workflow's own `name`, `enabled`, `trigger_type` and `step_count` travel with the counts, because the counts alone are ambiguous: no running executions means one thing on an enabled workflow and another on a paused one.
+ *
+ * Requires the `workflows:read` scope — View your automation workflows and their runs.
+ */
+ get: operations["v1GetWorkflowStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/verify": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateMailboxBody: {
- displayName?: string;
- /**
- * Format: uuid
- * @description A VERIFIED domain belonging to this project.
- */
- domainId: string;
- /** @description The part before the `@`, e.g. `support`. Lowercased server-side. */
- localPart: string;
+ get?: never;
+ put?: never;
+ /**
+ * Validate an email address
+ * @description Open endpoint (no auth required) that checks an email for syntax, MX records, disposable domains, and plus-addressing. Used by the marketing site verifier.
+ */
+ post: operations["verifyEmailAddress"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List user webhooks
+ * @description List all user-managed outbound webhooks for the auth'd project (secrets are not returned).
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["listWebhooks"];
+ put?: never;
+ /**
+ * Create a webhook
+ * @description Register a new outbound webhook. The plaintext signing secret is returned exactly once — store it securely.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["createWebhook"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get a webhook
+ * @description Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["getWebhook"];
+ put?: never;
+ post?: never;
+ /**
+ * Delete a webhook
+ * @description Hard-delete a webhook. Cascades to all WebhookCall rows.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ delete: operations["deleteWebhook"];
+ options?: never;
+ head?: never;
+ /**
+ * Update a webhook
+ * @description Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ patch: operations["updateWebhook"];
+ trace?: never;
+ };
+ "/api/webhooks/{id}/calls": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List recent webhook calls
+ * @description Cursor-paginated list of recent delivery attempts for a single webhook.
+ *
+ * Requires the `webhooks:read` scope — View your webhook endpoints and their delivery history.
+ */
+ get: operations["listWebhookCalls"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/webhooks/{id}/rotate-secret": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Rotate the webhook signing secret
+ * @description Generate a new shared secret. Returns the new plaintext secret exactly once.
+ *
+ * Requires the `webhooks:write` scope — Create, edit, and delete your webhook endpoints.
+ */
+ post: operations["rotateWebhookSecret"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+interface components {
+ schemas: {
+ /** @description Body for POST /api/domains. `projectId` is optional for API-key auth (derived from key) and required for session auth. `region` pins the SES region; on the first domain it locks the project, after that it must match the project's region. `stream` assigns the identity to transactional or marketing traffic at creation; omit it to leave the identity serving both. */
+ AddDomainBody: {
+ domain: string;
+ /** Format: uuid */
+ projectId?: string;
/**
- * Format: uuid
- * @description Defaults to the project the credential resolves to. Naming a different one is refused.
+ * @description Override SES region for this domain. Defaults to the project region or the env default. Required to match an existing project region.
+ * @enum {string}
*/
- projectId?: string;
- /** @description NOT IMPLEMENTED — sending any value answers 400. */
- quotaBytes?: number;
+ region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ stream?: components["schemas"]["SendingStream"];
+ /** @description Make this the project's default identity for `stream`. Requires `stream`. Setting it demotes whichever identity held it. */
+ streamDefault?: boolean;
};
- /** @description Body for POST /api/templates. */
- CreateTemplate: {
- body: string;
- description?: string;
- /** Format: email */
- from: string;
- fromName?: string | null;
- name: string;
+ /** @description Body for POST /api/suppression — manually add an email to the suppression list. */
+ AddSuppression: {
/** Format: email */
- replyTo?: string | null;
- subject: string;
+ email: string;
/**
- * @default MARKETING
+ * @default MANUAL
* @enum {string}
*/
- type: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
};
- /** @description Body for POST /api/webhooks — register a user webhook for one or more events. */
- CreateWebhook: {
- eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** Format: uri */
- url: string;
+ /** @description Campaign counters and engagement over the window. */
+ AnalyticsCampaignStatsV1: {
+ /** @description Campaigns in DRAFT or SCHEDULED. */
+ active: number;
+ average_click_rate: number;
+ /** @description Percentage, one decimal place. */
+ average_open_rate: number;
+ completed: number;
+ total: number;
+ window: components["schemas"]["AnalyticsWindowV1"];
};
- /** @description A sending domain registered with SES. */
- Domain: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- dkim?: {
- name: string;
- type: string;
- value: string;
+ /** @description Daily email counters across the window. Every day in range is present, zero-filled. */
+ AnalyticsTimeseriesV1: {
+ data: {
+ bounces: number;
+ clicks: number;
+ /** Format: date-time */
+ date: string;
+ delivered: number;
+ emails: number;
+ opens: number;
+ }[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description Sent campaigns ranked by open rate. */
+ AnalyticsTopCampaignsV1: {
+ data: {
+ click_rate: number;
+ clicked: number;
+ /** Format: uuid */
+ id: string;
+ open_rate: number;
+ opened: number;
+ sent: number;
+ subject: string;
}[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description The time range this response was computed over, after the 90-day clamp. */
+ AnalyticsWindowV1: {
+ /** Format: date-time */
+ from: string;
+ /** Format: date-time */
+ to: string;
+ };
+ /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
+ ApiKey: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** Format: uuid */
+ domainId: string | null;
/** Format: uuid */
id: string;
- /** @description Custom MAIL FROM subdomain SES has on record (normally `sendly.`). */
- mailFromDomain?: string | null;
+ /** @description Last 4 characters of the token — the only fragment of the secret that survives creation. */
+ lastFour: string;
/**
- * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
- * @enum {string|null}
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ lastUsedAt: string | null;
+ /**
+ * @description The coarse preset the key was minted under. It picks the token prefix (`sk_` vs `pk_`) and the rate-limit window, and it is the key's authority ONLY while `scopes` is empty — which is true just of keys minted before scopes existed. Read `scopes` to learn what a key can do.
+ * @enum {string}
*/
- mailFromStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ legacyGrantPreset: "FULL" | "SENDING_ONLY";
+ /**
+ * @description Where mail sent with this key may go. `LIVE` sends from your verified domains. `TEST` can only send from this project's sandbox address, whose recipient list is your own verified account addresses, so a test key cannot reach a customer whatever it is asked to do. Fixed at creation.
+ * @enum {string}
+ */
+ mode: "LIVE" | "TEST";
name: string;
/** Format: uuid */
projectId: string;
- region?: string | null;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description Set once the key is revoked. Revoked keys are NOT filtered out of list/get responses.
*/
- updatedAt: string;
- verified: boolean;
+ revokedAt: string | null;
+ /** @description The explicit scope grant this key carries. Empty on a key minted before this column existed — that row's grant is derived from `legacyGrantPreset` instead at request time. */
+ scopes: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test" | "deliverability:read" | "mailboxes:send" | "validation:read" | "validation:write" | "topics:read" | "topics:write" | "lists:read" | "lists:write")[];
};
- /** @description List of all domains for the auth'd project. */
- DomainListResponse: {
- data: components["schemas"]["Domain"][];
+ /** @description Every API key on the project, including revoked ones — filter on `revokedAt` for live keys. */
+ ApiKeyListResponse: {
+ data: components["schemas"]["ApiKey"][];
/** @enum {boolean} */
success: true;
};
- /** @description Outcome of a verification check against SES. */
- DomainVerificationStatus: {
- dkim?: {
- name: string;
- type: string;
- value: string;
- }[];
- mailFromDomain?: string | null;
- /**
- * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
- * @enum {string|null}
- */
- mailFromStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
- mxRecords?: string[];
- verified: boolean;
- };
- /** @description A sent (or queued) transactional email. */
- Email: {
+ /** @description An IMAP/SMTP credential for one mailbox, described but never reproduced. */
+ AppPassword: {
+ /** Format: date-time */
+ createdAt: string;
+ /** Format: uuid */
+ id: string;
+ /** @description The last four characters of the secret — enough to tell two credentials apart, and nothing more. */
+ lastFour: string;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description Null until a mail client has authenticated with it at least once.
*/
- createdAt: string;
- error?: string | null;
- from: string;
+ lastUsedAt: string | null;
+ /** @description What the credential is for, e.g. `Thunderbird on my laptop`. */
+ name: string;
+ /** @description Which protocols this password may authenticate. `imap` reads, `smtp` sends. */
+ scopes: ("imap" | "smtp")[];
+ };
+ /** @description A newly created app password, handed over as a one-time link rather than as a secret. */
+ AppPasswordReveal: {
/** Format: uuid */
id: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- status: "PENDING" | "SENT" | "DELIVERED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED";
- subject: string;
- tags: string[];
- to: string;
/**
* Format: date-time
- * @description ISO 8601 datetime string
+ * @description When the link stops working. Five minutes after creation; the password itself does not expire.
*/
- updatedAt: string;
- };
- /** @description Single email with its events. */
- EmailGetResponse: {
- data: components["schemas"]["Email"];
- /** @enum {boolean} */
- success: true;
- };
- /** @description Cursor-paginated list of emails. */
- EmailListResponse: {
- data: components["schemas"]["Email"][];
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A single-use link that shows the password once, in a browser. Opening it requires a signed-in Sendly session belonging to a project admin — the connection that created the password cannot open it, and the second attempt to open it fails whoever makes it.
+ */
+ revealUrl: string;
};
- /** @description Receipt for a sandbox test send. */
- EmailTestV1: {
+ /** @description Body for PATCH /api/domains/{id}. */
+ AssignDomainStream: {
/**
* Format: email
- * @description This project's sandbox sender — resolved server-side, never from the body.
+ * @description The address a send on this stream uses when it names none. Must be on this identity's own host — a default pointing elsewhere would go out unsigned by the name in the From header.
*/
- from: string;
+ defaultFromAddress?: string | null;
/**
- * Format: uuid
- * @description The Email row this send created.
+ * @description Which traffic this identity carries. `null` unassigns it, returning it to serving every stream and clearing its default flag and default address.
+ * @enum {string|null}
*/
+ stream?: "TRANSACTIONAL" | "MARKETING" | null;
+ /** @description Make this the project's default identity for its stream, demoting whichever held it. */
+ streamDefault?: boolean;
+ };
+ /** @description Per-row result in a batch send response. */
+ BatchEntryResult: {
+ data?: components["schemas"]["SendEmailData"];
+ error?: {
+ code: string;
+ message: string;
+ };
+ index: number;
+ /** @enum {string} */
+ status: "ok" | "error";
+ };
+ /** @description Batch send wrapper. Up to 100 entries. */
+ BatchSendBody: {
+ emails: components["schemas"]["SendEmail"][];
+ };
+ /** @description Multi-status response for `POST /api/emails/batch`. HTTP 207 if any entry failed, else 200. */
+ BatchSendResponse: {
+ data: components["schemas"]["BatchEntryResult"][];
+ success: boolean;
+ };
+ /** @description A campaign as exposed on the v1 API. */
+ CampaignV1: {
+ /** @enum {string} */
+ audience_type: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ /** Format: date-time */
+ created_at: string;
+ /** Format: uuid */
id: string;
+ /** Format: uuid */
+ list_id: string | null;
+ name: string;
+ /** Format: date-time */
+ scheduled_at: string | null;
+ /** Format: date-time */
+ sent_at: string | null;
+ stats: {
+ bounced: number;
+ clicked: number;
+ delivered: number;
+ opened: number;
+ sent: number;
+ total_recipients: number;
+ };
+ /** @enum {string} */
+ status: "DRAFT" | "SCHEDULED" | "SENDING" | "PAUSED" | "SENT" | "CANCELLED";
+ subject: string;
+ /** Format: uuid */
+ topic_id: string | null;
+ };
+ /** @description Body for POST /api/v1/campaigns. `segment_id` is required when `audience_type` is `SEGMENT`, `audience_condition` when it is `FILTERED`, and `list_id` when it is `LIST`. */
+ CampaignV1Create: {
+ audience_condition?: components["schemas"]["FilterConditionV1"];
/**
- * @description Always true. It is here so a model relaying this result cannot describe a test send as a real one: the message came from the shared sandbox domain and could only reach the project owner's own inbox.
- * @enum {boolean}
+ * @description `ALL` — every subscribed contact. `FILTERED` — the contacts matching `audience_condition`. `SEGMENT` — the members of `segment_id`. `LIST` — the CONFIRMED members of `list_id`, which is the only audience that honours double opt-in: a member who never confirmed, or who unsubscribed, is not mailed.
+ * @enum {string}
*/
- sandbox: true;
+ audience_type: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ body: string;
+ description?: string;
/**
- * @description Delivery status at the moment of the response — `PENDING` for a send still queued.
+ * @default MARKETING
* @enum {string}
*/
- status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
/**
* Format: email
- * @description The recipient the message was queued for.
+ * @description Sender address. Its domain must be verified for this project.
*/
- to: string;
- };
- /** @description Receipt for a single transactional send. */
- EmailV1: {
+ from: string;
+ from_name?: string | null;
/**
- * Format: email
- * @description The sender actually used. Worth reading rather than assuming: it is resolved server-side and may come from the template when the request named none.
+ * Format: uuid
+ * @description Required when `audience_type` is `LIST`. Only that list's CONFIRMED members receive the campaign.
*/
- from: string;
+ list_id?: string;
+ name: string;
+ /** Format: email */
+ reply_to?: string | null;
+ /** Format: uuid */
+ segment_id?: string;
+ subject: string;
/**
* Format: uuid
- * @description The Email row this send created. Quote it in support requests.
+ * @description The subject this campaign is about. A contact who unsubscribed from the topic is excluded WHATEVER the audience type — a topic opt-out is a standing answer, not an audience filter, so it cannot be routed around by selecting a different audience. Ignored for `TRANSACTIONAL` campaigns, which a marketing preference does not cancel.
*/
+ topic_id?: string | null;
+ };
+ /** @description Acknowledgement that a campaign was deleted. */
+ CampaignV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
id: string;
+ };
+ /** @description A campaign recipient whose send did not complete. */
+ CampaignV1Failure: {
+ /** Format: uuid */
+ contact_id: string;
+ /** @description The recipient the send was for. */
+ email: string;
+ /** Format: date-time */
+ failed_at: string;
/**
- * @description Delivery status at the moment of the response — `PENDING` for a send the worker has not picked up yet, which is the usual answer. Later states arrive via webhooks, not here.
- * @enum {string}
+ * Format: uuid
+ * @description Ledger row id. Pass the last one as `after` to page.
*/
- status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ id: string;
+ reason: string | null;
+ };
+ /** @description Cursor-paginated list of a campaign's failed sends. */
+ CampaignV1FailureList: {
+ data: components["schemas"]["CampaignV1Failure"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ /** @description Every FAILED row on this campaign, not just this page. */
+ total: number;
+ };
+ /** @description Cursor-paginated list of campaigns. */
+ CampaignV1List: {
+ data: components["schemas"]["CampaignV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Acknowledgement that a retry of a campaign's failed sends began. */
+ CampaignV1RetryFailed: {
+ /** Format: uuid */
+ id: string;
+ /** @description How many FAILED rows the retry walk was started for, counted when it was queued. */
+ queued: number;
+ };
+ /** @description Body for POST /api/v1/campaigns/{id}/send. */
+ CampaignV1Send: {
/**
- * Format: email
- * @description The recipient the message was queued for.
+ * Format: date-time
+ * @description RFC 3339 timestamp, strictly in the future. A numeric UTC offset (`+02:00`) is accepted as well as `Z`. Omit to start sending immediately.
*/
- to: string;
+ scheduled_for?: string;
};
- /** @description Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`. */
- Error: {
- error: {
- code: string;
+ /** @description Materialized delivery and engagement counters for one campaign. */
+ CampaignV1Stats: {
+ bounce_rate: number;
+ bounced: number;
+ click_rate: number;
+ clicked: number;
+ delivered: number;
+ delivery_rate: number;
+ open_rate: number;
+ opened: number;
+ sent: number;
+ total_recipients: number;
+ };
+ /** @description Body for PATCH /api/v1/campaigns/{id}. All fields optional. */
+ CampaignV1Update: {
+ audience_condition?: components["schemas"]["FilterConditionV1"];
+ /** @enum {string} */
+ audience_type?: "ALL" | "FILTERED" | "SEGMENT" | "LIST";
+ body?: string;
+ description?: string;
+ /** @enum {string} */
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /**
+ * Format: email
+ * @description Sender address. Its domain must be verified for this project.
+ */
+ from?: string;
+ from_name?: string | null;
+ /**
+ * Format: uuid
+ * @description Required when `audience_type` is `LIST`. Only that list's CONFIRMED members receive the campaign.
+ */
+ list_id?: string;
+ name?: string;
+ /** Format: email */
+ reply_to?: string | null;
+ /** Format: uuid */
+ segment_id?: string;
+ subject?: string;
+ /**
+ * Format: uuid
+ * @description The subject this campaign is about. A contact who unsubscribed from the topic is excluded WHATEVER the audience type — a topic opt-out is a standing answer, not an audience filter, so it cannot be routed around by selecting a different audience. Ignored for `TRANSACTIONAL` campaigns, which a marketing preference does not cancel.
+ */
+ topic_id?: string | null;
+ };
+ /** @description Body for POST /api/mailboxes/{id}/messages — a new outbound message from a hosted mailbox. */
+ ComposeMailboxMessage: {
+ bcc?: string[];
+ body: string;
+ cc?: string[];
+ subject: string;
+ to: string[];
+ };
+ /** @description A subscriber/contact within a project. */
+ Contact: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ customFields?: {
+ [key: string]: unknown;
+ } | null;
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ /** Format: uuid */
+ projectId: string;
+ subscribed: boolean;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Bulk create up to 1000 contacts. */
+ ContactBulkCreateBody: {
+ contacts: components["schemas"]["CreateContact"][];
+ };
+ /** @description Bulk delete contacts. Provide either `ids` or `emails` (max 1000 each). */
+ ContactBulkDeleteBody: {
+ emails?: string[];
+ ids?: string[];
+ };
+ /** @description Cursor-paginated list of contacts. */
+ ContactListResponse: {
+ data: {
+ data: components["schemas"]["Contact"][];
+ hasMore: boolean;
+ /** @description Cursor for the next page, or null on the last page. */
+ nextCursor: string | null;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Everything this contact has said about what they want. `subscribed` on a topic already folds in `default_opt_in`, so a contact who has never answered still reads correctly. */
+ ContactTopicPreferencesV1: {
+ contact_id: string;
+ /** @description The global marketing opt-out, which OUTRANKS every topic. False means no marketing reaches this contact whatever the topics below say. */
+ subscribed: boolean;
+ topics: {
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ pending: boolean;
+ /** @description The EFFECTIVE answer: what the send path concludes for this contact today. */
+ subscribed: boolean;
+ topic_id: string;
+ }[];
+ };
+ /** @description A contact as exposed on the v1 API. */
+ ContactV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ email: string;
+ /** Format: uuid */
+ id: string;
+ subscribed: boolean;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/contacts. */
+ ContactV1Create: {
+ /** @description Arbitrary JSON stored on the contact and available to templates as `{{ variables }}`. */
+ custom_fields?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ /** Format: email */
+ email: string;
+ /** @default true */
+ subscribed: boolean;
+ };
+ /** @description Acknowledgement that a contact was deleted. */
+ ContactV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of contacts. */
+ ContactV1List: {
+ data: components["schemas"]["ContactV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/contacts/{id}. `email` is deliberately absent: an address is the contact's identity on this API, and changing it in place would silently rewrite what every earlier send was addressed to. Create the new address instead. */
+ ContactV1Update: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ subscribed?: boolean;
+ };
+ CreateApiKeyBody: {
+ /** Format: uuid */
+ domainId?: string | null;
+ /** @enum {string} */
+ legacyGrantPreset?: "FULL" | "SENDING_ONLY";
+ /**
+ * @description `LIVE` (default) or `TEST`. A test key can only send from the project's sandbox address, which accepts only the project's own verified account addresses as recipients, so it cannot reach a customer. Its sends are real sends down the same pipeline and are marked `sentInTestMode`. Fixed at creation.
+ * @enum {string}
+ */
+ mode?: "LIVE" | "TEST";
+ name: string;
+ /** @description The explicit grant the new key will carry. Omitted ⇒ materialised from `legacyGrantPreset`. A `SENDING_ONLY` key may carry only `emails:send`. */
+ scopes?: ("emails:send" | "emails:read" | "contacts:read" | "contacts:write" | "campaigns:read" | "campaigns:write" | "segments:read" | "segments:write" | "workflows:read" | "workflows:write" | "templates:read" | "templates:write" | "domains:read" | "domains:write" | "webhooks:read" | "webhooks:write" | "suppression:read" | "suppression:write" | "analytics:read" | "usage:read" | "events:read" | "events:write" | "projects:read" | "projects:write" | "api-keys:read" | "api-keys:write" | "campaigns:send" | "mailboxes:read" | "mailboxes:write" | "emails:test" | "deliverability:read" | "mailboxes:send" | "validation:read" | "validation:write" | "topics:read" | "topics:write" | "lists:read" | "lists:write")[];
+ };
+ /** @description Body for POST /api/mailboxes/:id/app-passwords. */
+ CreateAppPassword: {
+ name: string;
+ /**
+ * @default [
+ * "imap",
+ * "smtp"
+ * ]
+ */
+ scopes: ("imap" | "smtp")[];
+ };
+ /** @description Body for POST /api/contacts and /api/contacts/upsert. */
+ CreateContact: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ customFields?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ /** @default true */
+ subscribed: boolean;
+ };
+ CreateMailboxBody: {
+ displayName?: string;
+ /**
+ * Format: uuid
+ * @description A VERIFIED domain belonging to this project.
+ */
+ domainId: string;
+ /** @description The part before the `@`, e.g. `support`. Lowercased server-side. */
+ localPart: string;
+ /**
+ * Format: uuid
+ * @description Defaults to the project the credential resolves to. Naming a different one is refused.
+ */
+ projectId?: string;
+ /** @description NOT IMPLEMENTED — sending any value answers 400. */
+ quotaBytes?: number;
+ };
+ /** @description Body for POST /api/snippets. */
+ CreateSnippet: {
+ body: string;
+ description?: string | null;
+ name: string;
+ };
+ /** @description Body for POST /api/templates. */
+ CreateTemplate: {
+ body: string;
+ description?: string;
+ /**
+ * @default MARKETING
+ * @enum {string}
+ */
+ emailCategory: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from: string;
+ fromName?: string | null;
+ name: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject: string;
+ };
+ /** @description Body for POST /api/webhooks — register a user webhook for one or more events. */
+ CreateWebhook: {
+ eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uri */
+ url: string;
+ };
+ /** @description A composed answer to why mail from one domain may not be arriving: its DNS identity, the project's recent delivery outcomes, one address's suppression state, and the findings drawn from them. */
+ DeliverabilityDiagnosisV1: {
+ address: string | null;
+ /** Format: date-time */
+ checked_at: string;
+ domain: string;
+ /** @description What is wrong, worst first. An empty array means nothing here explains a delivery problem. */
+ findings: components["schemas"]["DeliverabilityFindingV1"][];
+ identity: components["schemas"]["DeliverabilityIdentityV1"];
+ recent_delivery: components["schemas"]["DeliverabilityRecentDeliveryV1"];
+ suppression: components["schemas"]["DeliverabilitySuppressionV1"];
+ };
+ /**
+ * @description `blocking`: mail from this domain cannot be delivered as configured. `degraded`: it delivers, but inbox placement or sender reputation is at risk. `info`: worth knowing, nothing to fix.
+ * @enum {string}
+ */
+ DeliverabilityFindingSeverityV1: "blocking" | "degraded" | "info";
+ /** @description One diagnosed problem, with its fix. */
+ DeliverabilityFindingV1: {
+ /** @description Stable identifier for this finding, e.g. `domain_not_verified`. Branch on this, not on `summary`. */
+ code: string;
+ /** @description What to do about it. */
+ remedy: string;
+ severity: components["schemas"]["DeliverabilityFindingSeverityV1"];
+ /** @description What is wrong, in one sentence. */
+ summary: string;
+ };
+ /** @description The sending identity's DNS health, as last refreshed. */
+ DeliverabilityIdentityV1: {
+ /**
+ * @description DKIM signing. This is the one that decides whether Sendly will send from the domain at all.
+ * @enum {string|null}
+ */
+ dkim_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * @description The DMARC policy published at `_dmarc.`.
+ * @enum {string|null}
+ */
+ dmarc_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * Format: date-time
+ * @description When the DNS refresh job last looked. These statuses are a CACHE, not a live lookup.
+ */
+ last_checked_at: string | null;
+ mail_from_domain: string | null;
+ /** @description Raw SES `CustomMailFromStatus` (`Pending`/`Success`/`Failed`/`TemporaryFailure`), or `NotConfigured`. `Failed` means SES silently fell back to `amazonses.com`, which is why the value is reported rather than folded into a boolean. */
+ mail_from_domain_status: string | null;
+ /**
+ * @description Inbound receiving only. Null unless the domain has receiving enabled.
+ * @enum {string|null}
+ */
+ mx_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description Whether this project has a domain record at all. False makes every other field null. */
+ registered: boolean;
+ /**
+ * @description SPF alignment for the sending identity.
+ * @enum {string|null}
+ */
+ spf_status: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ verified: boolean;
+ };
+ /** @description Delivery outcomes over the requested window. */
+ DeliverabilityRecentDeliveryV1: {
+ /** @description Bounced ÷ sent (0–1), or null when nothing was sent in the window. */
+ bounce_rate: number | null;
+ bounced: number;
+ complained: number;
+ complaint_rate: number | null;
+ delivered: number;
+ failed: number;
+ /**
+ * @description PROJECT-WIDE, not per-domain, and said so rather than implied. `Email` has no sending-domain column, so narrowing these counters to one domain would mean a scan over every row the project has ever sent. A project that sends from one domain — most of them — can read these as that domain's.
+ * @enum {string}
+ */
+ scope: "project";
+ sent: number;
+ window_days: number;
+ };
+ /** @description Null unless the request named an `address`. */
+ DeliverabilitySuppressionV1: {
+ /** @enum {string|null} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE" | null;
+ /** @enum {string|null} */
+ source: "SES_WEBHOOK" | "API" | "DASHBOARD" | null;
+ suppressed: boolean;
+ /** Format: date-time */
+ suppressed_at: string | null;
+ } | null;
+ /** @description One DMARC aggregate (RUA) report. */
+ DmarcReportV1: {
+ fail_count: number;
+ id: string;
+ /** @description The reporting receiver, e.g. `google.com`. */
+ org_name: string;
+ /** @description Messages DMARC-ALIGNED (SPF or DKIM aligned and passing), from `policy_evaluated`. Not the raw auth results: a message can pass SPF for a domain that is not the one in its From header, which is the case DMARC exists to catch. */
+ pass_count: number;
+ /** @description The domain of yours the report is about. */
+ policy_domain: string;
+ /** Format: date-time */
+ range_begin: string;
+ /** Format: date-time */
+ range_end: string;
+ /** Format: date-time */
+ received_at: string;
+ /** @description The receiver's own id for this report. */
+ report_id: string;
+ /** @description Per-sending-source rows, as the receiver reported them. */
+ sources: {
+ count: number;
+ disposition: string;
+ dkim: string;
+ header_from: string;
+ source_ip: string;
+ spf: string;
+ }[];
+ total_count: number;
+ };
+ /** @description Cursor-paginated DMARC aggregate reports, newest window first. */
+ DmarcReportV1List: {
+ data: components["schemas"]["DmarcReportV1"][];
+ has_more: boolean;
+ /** @description Whether this deployment has a DMARC report intake mailbox configured. When `false` no report can ever arrive, so an empty `data` means the feature is off rather than that your domains are clean — the two are otherwise indistinguishable. */
+ intake_configured: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A sending identity: one domain registered with SES, with its own DKIM keys, its own MAIL FROM and its own reputation. */
+ Domain: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** @description The address a send on this stream uses when it names none. Always on this identity's own host. */
+ defaultFromAddress?: string | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ dkimStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description SES DKIM tokens to publish as CNAME records before the domain can verify. */
+ dkimTokens?: string[] | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ dmarcStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /** @description The bare domain, e.g. `mail.acme.com`. */
+ domain: string;
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ lastHealthCheckAt?: string | null;
+ /** @description Custom MAIL FROM subdomain SES has on record (normally `sendly.`). */
+ mailFromDomain?: string | null;
+ /**
+ * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
+ * @enum {string|null}
+ */
+ mailFromDomainStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ /** Format: uuid */
+ projectId: string;
+ /** @description Whether inbound mail for this domain is routed to Sendly mailboxes. */
+ receivingEnabled: boolean;
+ region?: string | null;
+ /**
+ * @description Result of the last DNS check for this record type.
+ * @enum {string|null}
+ */
+ spfStatus?: "NOT_CHECKED" | "PENDING" | "VERIFIED" | "FAILED" | null;
+ /**
+ * @description Which traffic this identity carries. `null` means unassigned, and an unassigned identity carries every stream — the behaviour of every domain added before per-stream identities. A send whose stream does not match an ASSIGNED identity is refused.
+ * @enum {string|null}
+ */
+ stream?: "TRANSACTIONAL" | "MARKETING" | null;
+ /** @description Whether this is the project's default identity for its stream — the one a send picks when it names no from-address. At most one per (project, stream). */
+ streamDefault?: boolean;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ verified: boolean;
+ };
+ /** @description List of all domains for the auth'd project. */
+ DomainListResponse: {
+ data: components["schemas"]["Domain"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A sending domain as exposed on the v1 API. */
+ DomainV1: {
+ /** Format: date-time */
+ created_at: string;
+ default_from_address: string | null;
+ dkim_verified: boolean;
+ domain: string;
+ /** Format: uuid */
+ id: string;
+ mail_from_domain: string | null;
+ /** @description SES's CustomMailFromStatus for `mail_from_domain` — the subdomain that carries the bounce path, NOT the status of any From address. */
+ mail_from_domain_status: string | null;
+ region: string | null;
+ stream: components["schemas"]["SendingStream"] & (string | null);
+ stream_default: boolean;
+ /** Format: date-time */
+ updated_at: string;
+ verified: boolean;
+ };
+ /** @description Body for POST /api/v1/domains. */
+ DomainV1Create: {
+ domain: string;
+ /**
+ * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
+ * @enum {string}
+ */
+ region?: "us-east-1" | "us-west-2" | "eu-west-1";
+ stream?: components["schemas"]["SendingStream"] & unknown;
+ /** @description Make this the project's default identity for `stream`. Requires `stream`. */
+ stream_default?: boolean;
+ };
+ /** @description Acknowledgement that a sending domain was removed. */
+ DomainV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of sending domains. */
+ DomainV1List: {
+ data: components["schemas"]["DomainV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Outcome of a verification check against SES. */
+ DomainVerificationStatus: {
+ /** @enum {string} */
+ dkimStatus: "VERIFIED" | "PENDING" | "FAILED";
+ /** @enum {string} */
+ dmarcStatus: "VERIFIED" | "FAILED" | "NOT_CHECKED";
+ domain: string;
+ mailFromDomain: string | null;
+ /**
+ * @description SES custom MAIL FROM setup state. Only `Success` means SES is using it.
+ * @enum {string|null}
+ */
+ mailFromDomainStatus?: "Pending" | "Success" | "Failed" | "TemporaryFailure" | "NotConfigured" | null;
+ /** @enum {string} */
+ spfStatus: "VERIFIED" | "FAILED" | "NOT_CHECKED";
+ /** @description Raw SES DKIM verification status, e.g. `Success` or `Pending`. */
+ status: string;
+ /** @description DKIM tokens SES still has to report. Absent once verification has resolved. */
+ tokens?: string[];
+ verified: boolean;
+ };
+ /** @description Body for POST /api/mailboxes/{id}/drafts — ask for help writing, never for sending. */
+ DraftMailboxMessage: {
+ brief?: string;
+ draft?: string;
+ instruction?: string;
+ /** @enum {string} */
+ mode: "draft" | "rewrite" | "subject";
+ recipientContext?: string;
+ senderAddress?: string;
+ /** @enum {string} */
+ tone?: "friendly" | "neutral" | "formal" | "apologetic" | "direct";
+ };
+ /** @description A sent (or queued) transactional email. */
+ Email: {
+ /**
+ * Format: date-time
+ * @description Bounced, or null.
+ */
+ bouncedAt: string | null;
+ /**
+ * Format: date-time
+ * @description First click, or null.
+ */
+ clickedAt: string | null;
+ /** @description Total clicks recorded. */
+ clicks: number;
+ /**
+ * Format: date-time
+ * @description Spam complaint, or null.
+ */
+ complainedAt: string | null;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description Accepted by the recipient's server, or null.
+ */
+ deliveredAt: string | null;
+ error?: string | null;
+ from: string;
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description First open, or null.
+ */
+ openedAt: string | null;
+ /** @description Total opens recorded. */
+ opens: number;
+ /** Format: uuid */
+ projectId: string;
+ /**
+ * Format: date-time
+ * @description Handed to the provider, or null.
+ */
+ sentAt: string | null;
+ status: components["schemas"]["EmailDeliveryStatus"];
+ subject: string;
+ tags: string[];
+ to: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /**
+ * @description Delivery lifecycle of the message. Engagement is reported separately.
+ * @enum {string}
+ */
+ EmailDeliveryStatus: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /** @description One email and its delivery history. */
+ EmailDetailResponse: {
+ data: components["schemas"]["EmailWithEvents"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description One transition in a message's delivery history. */
+ EmailEvent: {
+ /** Format: uuid */
+ id: string;
+ status: components["schemas"]["EmailDeliveryStatus"];
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @description Cursor-paginated list of emails. */
+ EmailListResponse: {
+ data: components["schemas"]["Email"][];
+ nextCursor?: string | null;
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A single email. */
+ EmailResponse: {
+ data: components["schemas"]["Email"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Receipt for a sandbox test send. */
+ EmailTestV1: {
+ /**
+ * Format: email
+ * @description This project's sandbox sender — resolved server-side, never from the body.
+ */
+ from: string;
+ /**
+ * Format: uuid
+ * @description The Email row this send created.
+ */
+ id: string;
+ /**
+ * @description Always true. It is here so a model relaying this result cannot describe a test send as a real one: the message came from the shared sandbox domain and could only reach the project owner's own inbox.
+ * @enum {boolean}
+ */
+ sandbox: true;
+ /**
+ * @description Delivery status at the moment of the response — `PENDING` for a send still queued.
+ * @enum {string}
+ */
+ status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /**
+ * Format: email
+ * @description The recipient the message was queued for.
+ */
+ to: string;
+ };
+ /** @description Receipt for a single transactional send. */
+ EmailV1: {
+ /**
+ * Format: email
+ * @description The sender actually used. Worth reading rather than assuming: it is resolved server-side and may come from the template when the request named none.
+ */
+ from: string;
+ /**
+ * Format: uuid
+ * @description The Email row this send created. Quote it in support requests.
+ */
+ id: string;
+ /**
+ * @description Delivery status at the moment of the response — `PENDING` for a send the worker has not picked up yet, which is the usual answer. Later states arrive via webhooks, not here.
+ * @enum {string}
+ */
+ status: "PENDING" | "SENDING" | "SENT" | "DELIVERED" | "RECEIVED" | "BOUNCED" | "FAILED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERY_DELAY" | "CANCELLED";
+ /**
+ * Format: email
+ * @description The recipient the message was queued for.
+ */
+ to: string;
+ };
+ EmailValidationBatchRequestV1: {
+ /** @description The addresses to check, at most 50. Every distinct DOMAIN costs a DNS round trip, so this endpoint is bounded by latency rather than payload size — validate a whole list with `POST /api/v1/lists/{id}/validation-runs`, which is a background job. */
+ emails: string[];
+ };
+ /** @description One verdict per address, in the order they were given. */
+ EmailValidationBatchV1: {
+ results: components["schemas"]["EmailValidationV1"][];
+ };
+ /** @description One page of a run's results. No total — a run over a large list holds millions of rows, and the run's own counters are the numbers worth reading. */
+ EmailValidationResultListV1: {
+ data: (components["schemas"]["EmailValidationV1"] & {
+ contact_id: string | null;
+ })[];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description One bulk validation run over a list. */
+ EmailValidationRunV1: {
+ /** Format: date-time */
+ completed_at: string | null;
+ /** Format: date-time */
+ created_at: string;
+ deliverable_count: number;
+ /** @description Set only on `failed`. Prose for an operator; never parse it. */
+ failure_reason: string | null;
+ id: string;
+ list_id: string | null;
+ /** @description Addresses checked so far. There is deliberately no total: a list changes size while a run walks it, so a denominator captured up front would be wrong by the time you read it. */
+ processed_count: number;
+ risky_count: number;
+ /** Format: date-time */
+ started_at: string | null;
+ /** @enum {string} */
+ status: "pending" | "running" | "completed" | "failed";
+ undeliverable_count: number;
+ };
+ /** @description One address's verdict, with the evidence behind it. */
+ EmailValidationV1: {
+ email: string;
+ /** @description The domain publishes MX records. */
+ has_mx_records: boolean;
+ /** @description A throwaway-inbox provider. The ONLY flag here that lowers the verdict. */
+ is_disposable: boolean;
+ /** @description A free/consumer provider (Gmail, Outlook). List-quality information, not a problem. */
+ is_personal: boolean;
+ /** @description The local part addresses a role (`support@`, `info@`), not a person. List-quality information: role mailboxes are deliverable and companies answer them. */
+ is_role_address: boolean;
+ /** @description Human-readable findings. Prose for a person to read — branch on `verdict`, never on these. */
+ reasons: string[];
+ verdict: components["schemas"]["EmailValidationVerdictV1"];
+ };
+ /**
+ * @description `deliverable`: the domain resolves and accepts mail, with no badness signal. `undeliverable`: the domain does not exist or publishes no MX records. `risky`: deliverable, but a throwaway-inbox provider — mailing it costs sender reputation. `unknown`: DNS did not answer in time, so this address was NOT checked. Ask again; never delete a contact on `unknown`.
+ * @enum {string}
+ */
+ EmailValidationVerdictV1: "deliverable" | "undeliverable" | "risky" | "unknown";
+ /** @description A transactional email together with its delivery history. */
+ EmailWithEvents: components["schemas"]["Email"] & {
+ /** @description Delivery transitions for this message, oldest first. NOT the custom events recorded with `POST /api/v1/events` — those are a separate resource. */
+ events: components["schemas"]["EmailEvent"][];
+ };
+ /** @description Standard error envelope returned by all 4xx/5xx responses. Migrated routes include `success: false`; 422 validation errors add `error.details.errors`. */
+ Error: {
+ error: {
+ code: string;
details?: {
errors: unknown[];
};
- message: string;
+ message: string;
+ };
+ /** @enum {boolean} */
+ success?: false;
+ };
+ /** @description Every distinct event name in the project, most frequent first. */
+ EventNamesV1: {
+ data: string[];
+ };
+ /** @description Per-name event counts over the applied window. */
+ EventStatsV1: {
+ data: {
+ count: number;
+ name: string;
+ }[];
+ window: components["schemas"]["AnalyticsWindowV1"];
+ };
+ /** @description Body for POST /api/v1/events. */
+ EventTrackV1: {
+ /**
+ * Format: uuid
+ * @description Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event.
+ */
+ contact_id?: string;
+ /** @description Event name, e.g. `user.signup`. */
+ name: string;
+ /** @description Arbitrary event payload. */
+ payload?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ };
+ /** @description A recorded custom event. */
+ EventV1: {
+ /** Format: uuid */
+ contact_id: string | null;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: uuid */
+ email_id: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ /** @description The payload recorded with the event, or null. */
+ payload: {
+ [key: string]: unknown;
+ } | null;
+ };
+ /** @description Cursor-paginated list of events, newest first. */
+ EventV1List: {
+ data: components["schemas"]["EventV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A filter condition: one or more groups combined with `logic`. */
+ FilterConditionV1: {
+ groups: components["schemas"]["FilterGroupV1"][];
+ /** @enum {string} */
+ logic: "AND" | "OR";
+ };
+ /** @description A group of filters. The filters inside one group ALWAYS combine with AND; `conditions` nests a further condition under this group, which is how OR-of-ANDs (and deeper) is expressed. */
+ FilterGroupV1: {
+ conditions?: components["schemas"]["FilterConditionV1"];
+ filters: components["schemas"]["SegmentFilterV1"][];
+ };
+ /** @description Success envelope carrying the affected resource's id, e.g. after a delete. */
+ IdResponse: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/lists/{id}/subscribe. */
+ ListSubscribe: {
+ /**
+ * @description Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add.
+ * @default false
+ */
+ allowResubscribe: boolean;
+ /** @description Custom fields to upsert onto the contact as part of subscribing. */
+ data?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ };
+ /** @description Result of a list-subscribe call. */
+ ListSubscribeResponse: {
+ data: {
+ /** @description Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm-subscription?token= to the contact. Valid for 24 hours. */
+ confirmToken?: string;
+ /** @description True when the membership row did not exist before this call. */
+ created: boolean;
+ /** Format: uuid */
+ membershipId: string;
+ /**
+ * @description Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user.
+ * @enum {string|null}
+ */
+ previousStatus: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED" | null;
+ /** @enum {string} */
+ status: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED";
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/lists/{id}/unsubscribe. */
+ ListUnsubscribe: {
+ /** Format: email */
+ email: string;
+ };
+ /** @description Echoes the address that was unsubscribed. */
+ ListUnsubscribeResponse: {
+ data: {
+ /** Format: email */
+ email: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A subscriber list as exposed on the v1 API. */
+ ListV1: {
+ /** Format: uuid */
+ confirmation_template_id: string | null;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ double_opt_in: boolean;
+ /** Format: uuid */
+ id: string;
+ /** @description Memberships in ANY status, including PENDING and UNSUBSCRIBED ones. */
+ member_count: number;
+ name: string;
+ redirect_url: string | null;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/lists. */
+ ListV1Create: {
+ /** Format: uuid */
+ confirmation_template_id?: string | null;
+ description?: string | null;
+ /**
+ * @description Require the contact to confirm before the membership becomes CONFIRMED. Sendly does NOT send the confirmation email — subscribing returns a `confirm_token` and you deliver `/api/lists/confirm-subscription?token=` to the contact yourself.
+ * @default false
+ */
+ double_opt_in: boolean;
+ name: string;
+ /**
+ * Format: uri
+ * @description Where a confirmed contact is sent after following the confirmation link.
+ */
+ redirect_url?: string | null;
+ };
+ /** @description Acknowledgement that a list was deleted. */
+ ListV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of subscriber lists. */
+ ListV1List: {
+ data: components["schemas"]["ListV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/lists/{id}. */
+ ListV1Update: {
+ /** Format: uuid */
+ confirmation_template_id?: string | null;
+ description?: string | null;
+ double_opt_in?: boolean;
+ name?: string;
+ /** Format: uri */
+ redirect_url?: string | null;
+ };
+ /** @description A receiving mailbox on one of the project's verified domains. */
+ Mailbox: {
+ /**
+ * Format: email
+ * @description The full mailbox address, e.g. `support@superbooks.io`.
+ */
+ address: string;
+ /** Format: date-time */
+ createdAt: string;
+ displayName: string | null;
+ /**
+ * Format: uuid
+ * @description The verified domain this mailbox lives on.
+ */
+ domainId: string;
+ /** Format: uuid */
+ id: string;
+ /** @description Always null. Mailbox quotas are not implemented — the value was never applied to the mail account — so this field reports the absence rather than a number nothing enforces. */
+ quotaBytes: number | null;
+ /**
+ * @description `PROVISIONING` while the mail account is being created, `ACTIVE` once it can receive, `SUSPENDED` when receiving is paused, `FAILED` when provisioning did not complete. A `FAILED` mailbox can be re-created with the same address — the retry reclaims the row.
+ * @enum {string}
+ */
+ status: "PROVISIONING" | "ACTIVE" | "SUSPENDED" | "FAILED";
+ };
+ /** @description A mailbox plus its IMAP/SMTP connection settings. */
+ MailboxDetail: components["schemas"]["Mailbox"] & {
+ /** @description Host, port and username for connecting a mail client. The PASSWORD is not here and is never returned by this endpoint — create an app password for that. */
+ settings: {
+ imap: {
+ host: string;
+ port: number;
+ /** @description Transport security, e.g. `SSL/TLS`. */
+ security: string;
+ /** @description The mailbox address — it is also the login. */
+ username: string;
+ };
+ smtp: {
+ host: string;
+ port: number;
+ /** @description Transport security, e.g. `SSL/TLS`. */
+ security: string;
+ /** @description The mailbox address — it is also the login. */
+ username: string;
+ };
+ };
+ };
+ /** @description RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface. */
+ Problem: {
+ /** @description Machine-readable lowercase error code, e.g. `scope_missing`. */
+ code: string;
+ /** @description Explanation specific to this occurrence. */
+ detail?: string;
+ /** @description Field-level failures. Present on 422 `validation_error` responses. */
+ errors?: {
+ code: string;
+ message: string;
+ /** @description RFC 6901 JSON Pointer to the offending field. */
+ pointer: string;
+ }[];
+ /** @description Request path the failure occurred on. */
+ instance?: string;
+ /** @description Correlation id — quote it in support requests. */
+ request_id?: string;
+ /** @description HTTP status code, repeated in the body. */
+ status: number;
+ /** @description Short, stable summary — the same for every occurrence of a `type`. */
+ title: string;
+ /**
+ * Format: uri
+ * @description Dereferenceable URI identifying the error class, anchored on the docs errors page.
+ */
+ type: string;
+ };
+ ProjectRecord: {
+ billingLimitCampaigns: number | null;
+ billingLimitInbound: number | null;
+ billingLimitTransactional: number | null;
+ billingLimitWorkflows: number | null;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ disabled: boolean;
+ disabledReason: string | null;
+ /** Format: uuid */
+ id: string;
+ /** @description ISO 639-1 code for customer-facing content. */
+ language: string;
+ name: string;
+ organizationId: string | null;
+ /** @description Local-part of the sandbox quick-start sender; null until first derived. */
+ sandboxHandle: string | null;
+ sesRegion: string | null;
+ stripeCustomerId: string | null;
+ stripeSubscriptionId: string | null;
+ /** @enum {string} */
+ tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description The project the presented credential is scoped to. */
+ ProjectV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description A disabled project sends nothing; every send is refused. */
+ disabled: boolean;
+ /** Format: uuid */
+ id: string;
+ /** @description ISO 639-1 code for customer-facing content. */
+ language: string;
+ name: string;
+ /** @description This project's quick-start sender, usable with no domain setup — but only to the project owner's own verified address, and under a daily cap. Null when none can be derived. */
+ sandbox_address: string | null;
+ /** @description Locked once the first domain is added. */
+ ses_region: string | null;
+ /** @enum {string} */
+ tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
+ };
+ /** @description Delivery outcomes for one recipient domain on one day. */
+ RecipientDomainStatsV1: {
+ bounced: number;
+ complained: number;
+ /**
+ * Format: date-time
+ * @description When the rollup job last rebuilt this row. These counts are a CACHE, refreshed hourly.
+ */
+ computed_at: string;
+ /** @description The UTC day these counts cover, as `YYYY-MM-DD`. */
+ day: string;
+ delivered: number;
+ /** @description The recipient's domain, lowercased: the part after the `@`. */
+ domain: string;
+ opened: number;
+ sent: number;
+ };
+ /** @description Cursor-paginated recipient-domain rollup, newest day first. */
+ RecipientDomainStatsV1List: {
+ data: components["schemas"]["RecipientDomainStatsV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A contact belonging to a segment. */
+ SegmentContactV1: {
+ /** Format: date-time */
+ created_at: string;
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ custom_fields: {
+ [key: string]: unknown;
+ };
+ email: string;
+ /** Format: uuid */
+ id: string;
+ subscribed: boolean;
+ };
+ /** @description Cursor-paginated list of the contacts belonging to a segment. */
+ SegmentContactV1List: {
+ data: components["schemas"]["SegmentContactV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description One comparison. `field` addresses a contact column (`email`, `createdAt`, …) or a `customFields.` path. `value` is whatever the operator compares against — its JSON type follows the field, and it is omitted entirely for the presence operators (`exists`, `notExists`). `unit` applies only to the relative-time operators (`within`, `olderThan`, and the `triggered*` family). */
+ SegmentFilterV1: {
+ field: string;
+ /** @enum {string} */
+ operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists" | "within" | "olderThan" | "triggered" | "triggeredWithin" | "triggeredOlderThan" | "notTriggered" | "notTriggeredWithin" | "isMemberOf";
+ /** @enum {string} */
+ unit?: "days" | "hours" | "minutes";
+ value?: unknown;
+ };
+ /** @description A segment as exposed on the v1 API. */
+ SegmentV1: {
+ condition: components["schemas"]["FilterConditionV1"] | null;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ /** Format: uuid */
+ id: string;
+ member_count: number;
+ name: string;
+ track_membership: boolean;
+ /** @enum {string} */
+ type: "DYNAMIC" | "STATIC";
+ /** Format: date-time */
+ updated_at: string;
+ };
+ /** @description Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`. */
+ SegmentV1Create: {
+ condition?: components["schemas"]["FilterConditionV1"];
+ description?: string;
+ name: string;
+ /**
+ * @description Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition.
+ * @default false
+ */
+ track_membership: boolean;
+ /**
+ * @default DYNAMIC
+ * @enum {string}
+ */
+ type: "DYNAMIC" | "STATIC";
+ };
+ /** @description Acknowledgement that a segment was deleted. */
+ SegmentV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of segments. */
+ SegmentV1List: {
+ data: components["schemas"]["SegmentV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment. */
+ SegmentV1Update: {
+ condition?: components["schemas"]["FilterConditionV1"];
+ description?: string;
+ name?: string;
+ track_membership?: boolean;
+ };
+ /** @description Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required. */
+ SendEmail: {
+ attachments?: {
+ content: string;
+ contentId?: string;
+ contentType: string;
+ /**
+ * @default attachment
+ * @enum {string}
+ */
+ disposition: "attachment" | "inline";
+ filename: string;
+ }[];
+ bcc?: string[];
+ body?: string;
+ cc?: string[];
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ from?: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ headers?: {
+ [key: string]: string;
+ };
+ name?: string;
+ /** Format: email */
+ reply?: string;
+ subject?: string;
+ subscribed?: boolean;
+ tags?: string[];
+ /** Format: uuid */
+ template?: string;
+ to: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ } | (string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ })[];
+ };
+ /** @description Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`. */
+ SendEmailData: {
+ emails: components["schemas"]["SendEmailRecipientResult"][];
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @description Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient. */
+ SendEmailRecipientResult: {
+ contact: {
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ };
+ /** Format: uuid */
+ email: string;
+ };
+ /** @description Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status. */
+ SendEmailResponse: {
+ data: components["schemas"]["SendEmailData"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for POST /api/v1/emails. Either `template` or `subject`+`body` is required, and `to` names exactly one recipient. */
+ SendEmailV1: {
+ attachments?: {
+ content: string;
+ contentId?: string;
+ contentType: string;
+ /**
+ * @default attachment
+ * @enum {string}
+ */
+ disposition: "attachment" | "inline";
+ filename: string;
+ }[];
+ bcc?: string[];
+ body?: string;
+ cc?: string[];
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ from?: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ headers?: {
+ [key: string]: string;
+ };
+ name?: string;
+ /** Format: email */
+ reply?: string;
+ subject?: string;
+ subscribed?: boolean;
+ tags?: string[];
+ /** Format: uuid */
+ template?: string;
+ /** @description The single recipient. Use `cc`/`bcc` to copy others on the same message. */
+ to: string | {
+ /** Format: email */
+ email: string;
+ name?: string;
+ };
+ };
+ /** @description Body for POST /api/v1/emails/test. `subject` and `body` are required; `to` defaults to the project owner's verified email, and `from` is refused. */
+ SendTestEmailV1: {
+ /** @description HTML body. Merge tags are rendered as on any other send. */
+ body: string;
+ /** @description NOT ACCEPTED. The sender is always this project's sandbox address, resolved server-side; naming one here is refused rather than ignored, so a request that expects a different sender never gets a success it would misread. Read `sandbox_address` from `GET /api/v1/projects` to learn the address, or `from` off this response. */
+ from?: string;
+ subject: string;
+ /**
+ * Format: email
+ * @description Where to send it. Optional — it defaults to the project owner's own verified account email, which is the only address a sandbox send may reach. Any other value is refused.
+ */
+ to?: string;
+ };
+ /**
+ * @description Which traffic this identity carries. Omit to leave it unassigned, which lets it carry every stream — that is what every domain added before per-stream identities does.
+ * @enum {string}
+ */
+ SendingStream: "TRANSACTIONAL" | "MARKETING";
+ /** @description A reusable fragment of template markup. */
+ Snippet: {
+ /** @description Template markup. Values it interpolates are escaped like any other. */
+ body: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ description?: string | null;
+ /** Format: uuid */
+ id: string;
+ /** @description The literal identifier a template includes with `{{> name}}`. */
+ name: string;
+ /** Format: uuid */
+ projectId: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Cursor-paginated list of snippets. */
+ SnippetListResponse: {
+ data: {
+ /** @description Cursor for the next page; omitted on the last page. */
+ cursor?: string;
+ data: components["schemas"]["Snippet"][];
+ hasMore: boolean;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Bare success envelope with no payload. */
+ SuccessEmpty: {
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A single suppressed-email record. */
+ Suppression: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** Format: email */
+ email: string;
+ /** Format: uuid */
+ id: string;
+ /** Format: uuid */
+ projectId: string;
+ /** @enum {string} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /**
+ * @description How far the suppression reaches. `PROJECT` is every record this API creates or returns today.
+ * @enum {string}
+ */
+ scope: "PROJECT" | "GLOBAL";
+ /** @enum {string} */
+ source: "SES_WEBHOOK" | "API" | "DASHBOARD";
+ };
+ /** @description Result of GET /api/suppression/{email} — whether the address is suppressed. */
+ SuppressionCheckResponse: {
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt?: string;
+ /** @enum {string} */
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ /** @enum {string} */
+ source?: "SES_WEBHOOK" | "API" | "DASHBOARD";
+ suppressed: boolean;
+ };
+ /** @description Cursor-paginated list of suppressions. NOTE: this route answers a bare body — there is no `{success, data}` envelope. */
+ SuppressionListResponse: {
+ items: components["schemas"]["Suppression"][];
+ /** @description Cursor for the next page, or `null` on the last page. Never omitted. */
+ nextCursor: string | null;
+ };
+ /** @description A suppressed address as exposed on the v1 API. */
+ SuppressionV1: {
+ /** Format: date-time */
+ created_at: string;
+ email: string;
+ /** @enum {string} */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ source: string;
+ };
+ /** @description Body for POST /api/v1/suppressions. */
+ SuppressionV1Create: {
+ /** Format: email */
+ email: string;
+ /**
+ * @default MANUAL
+ * @enum {string}
+ */
+ reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
+ /** @description Acknowledgement that an address was un-suppressed. */
+ SuppressionV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ email: string;
+ };
+ /** @description Cursor-paginated list of suppressed addresses. */
+ SuppressionV1List: {
+ data: components["schemas"]["SuppressionV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A reusable email template. */
+ Template: {
+ body: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /** @description Version counter, incremented by an update that changes the rendered content. A campaign records the version it sent, so this is how a caller tells 'the template changed since' from 'the template was renamed'. */
+ currentVersion: number;
+ description?: string | null;
+ /** @enum {string} */
+ emailCategory: "MARKETING" | "TRANSACTIONAL" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from: string;
+ fromName?: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ /** Format: uuid */
+ projectId: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ };
+ /** @description Cursor-paginated list of templates. */
+ TemplateListResponse: {
+ data: {
+ /** @description Cursor for the next page; omitted on the last page. */
+ cursor?: string;
+ data: components["schemas"]["Template"][];
+ hasMore: boolean;
+ total: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description An email template as exposed on the v1 API. */
+ TemplateV1: {
+ body: string;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ /** @enum {string} */
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ from: string;
+ from_name: string | null;
+ /** Format: uuid */
+ id: string;
+ name: string;
+ reply_to: string | null;
+ subject: string;
+ /** Format: date-time */
+ updated_at: string;
+ version: number;
+ };
+ /** @description Body for POST /api/v1/templates. */
+ TemplateV1Create: {
+ body: string;
+ description?: string | null;
+ /**
+ * @default MARKETING
+ * @enum {string}
+ */
+ email_category: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /**
+ * Format: email
+ * @description Sender address. Its domain must be verified for this project.
+ */
+ from: string;
+ from_name?: string | null;
+ name: string;
+ /** Format: email */
+ reply_to?: string | null;
+ subject: string;
+ };
+ /** @description Acknowledgement that a template was deleted. */
+ TemplateV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of templates. */
+ TemplateV1List: {
+ data: components["schemas"]["TemplateV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Body for PATCH /api/v1/templates/{id}. */
+ TemplateV1Update: {
+ body?: string;
+ description?: string | null;
+ /** @enum {string} */
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from?: string;
+ from_name?: string | null;
+ name?: string;
+ /** Format: email */
+ reply_to?: string | null;
+ subject?: string;
+ };
+ TopicCreateV1: {
+ default_opt_in?: boolean;
+ description?: string | null;
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ };
+ /** @description One page of the subjects this project mails about. */
+ TopicListV1: {
+ data: components["schemas"]["TopicV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ TopicSubscribeV1: {
+ /** Format: uuid */
+ contact_id: string;
+ /** @description True asks to subscribe, which starts a DOUBLE OPT-IN: the contact is parked at `pending` and a confirmation link is sent. There is no way to skip that from the API — a subscription an API caller asserts is not evidence the mailbox holder agreed, and treating it as consent is exactly what double opt-in exists to stop. False records an unsubscribe, which takes effect immediately. */
+ subscribed: boolean;
+ };
+ /**
+ * @description `subscribed`: mail them about this. `unsubscribed`: do not. `pending`: a confirmation link was sent and has NOT been clicked — never treat this as consent, which is the whole point of double opt-in.
+ * @enum {string}
+ */
+ TopicSubscriptionStatusV1: "pending" | "subscribed" | "unsubscribed";
+ TopicSubscriptionV1: {
+ /** @description Present only when this call started a double opt-in. Sendly does NOT send the confirmation email — you do, from your own verified domain, because it is your relationship with the contact and your sending reputation. The subscription stays `pending`, and is NOT mailed, until someone opens this link. */
+ confirmation_url: string | null;
+ /** Format: date-time */
+ confirmed_at: string | null;
+ contact_id: string;
+ status: components["schemas"]["TopicSubscriptionStatusV1"];
+ topic_id: string;
+ };
+ /** @description `key` is deliberately absent. It is the name every stored preference and every integration refers to, so changing it would silently orphan them. */
+ TopicUpdateV1: {
+ archived?: boolean;
+ default_opt_in?: boolean;
+ description?: string | null;
+ name?: string;
+ };
+ /** @description One subject this project mails about. */
+ TopicV1: {
+ /** @description Hidden from the preference centre and from new sends, WITHOUT discarding the opt-outs recorded against it. There is no delete for the same reason: deleting a topic would delete the choices people made about it. */
+ archived: boolean;
+ /** Format: date-time */
+ created_at: string;
+ /** @description What a contact with NO answer counts as. True is the honest default for a topic added after a list already exists: those contacts consented to hear from you, and inventing an opt-out they never asked for would silence mail they expect. False means the topic must be opted INTO, and absence means `not asked` rather than `no`. */
+ default_opt_in: boolean;
+ description: string | null;
+ id: string;
+ /** @description Stable, URL-safe, unique within the project. This is what a preference form and an API caller name the topic by, so it must survive a rename of `name` — which is the whole reason it exists beside one. */
+ key: string;
+ name: string;
+ /** @description Contacts who explicitly said yes. Excludes those covered only by `default_opt_in`. */
+ subscribed_count: number;
+ unsubscribed_count: number;
+ };
+ /** @description Body for POST /api/track — record a custom event for a contact. */
+ TrackEvent: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ data?: {
+ [key: string]: unknown;
+ };
+ /** Format: email */
+ email: string;
+ event: string;
+ subscribed?: boolean;
+ };
+ /** @description Response from POST /api/track. */
+ TrackEventResponse: {
+ data: {
+ /** Format: uuid */
+ contact: string;
+ /** Format: uuid */
+ event: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ timestamp: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses. */
+ UpdateContactBody: {
+ customFields?: {
+ [key: string]: unknown;
+ };
+ subscribed?: boolean;
+ };
+ /** @description Body for PATCH /api/snippets/{id}. */
+ UpdateSnippet: {
+ body?: string;
+ description?: string | null;
+ name?: string;
+ };
+ /** @description Body for PATCH /api/templates/{id}. */
+ UpdateTemplate: {
+ body?: string;
+ description?: string;
+ /** @enum {string} */
+ emailCategory?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
+ /** Format: email */
+ from?: string;
+ fromName?: string | null;
+ name?: string;
+ /** Format: email */
+ replyTo?: string | null;
+ subject?: string;
+ };
+ /** @description Body for PATCH /api/webhooks/{id}. */
+ UpdateWebhook: {
+ eventTypes?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** @enum {string} */
+ status?: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: uri */
+ url?: string;
+ };
+ /** @description Current email usage against the limits that are actually enforced. */
+ UsageV1: {
+ daily: {
+ /** @description Today's sends. Null when the counter could not be read. */
+ emails_sent: number | null;
+ limit: number;
+ /** @enum {string} */
+ trust_tier: "NEW" | "ESTABLISHED" | "TRUSTED";
+ };
+ monthly: {
+ categories: {
+ campaign: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ inbound: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ transactional: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ workflow: {
+ emails_sent: number;
+ limit: number | null;
+ };
+ };
+ emails_sent: number;
+ /** @description Monthly cap on the total. Null when per-category limits govern instead. */
+ limit: number | null;
+ };
+ /**
+ * @description `custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`.
+ * @enum {string}
+ */
+ plan: "free" | "pro" | "custom";
+ };
+ /** @description Body for POST /api/verify — validate email syntax, MX, disposable, etc. */
+ VerifyEmail: {
+ /** Format: email */
+ email: string;
+ };
+ /** @description Response from POST /api/verify — outcome of the syntax/MX/disposable check. */
+ VerifyEmailResponse: {
+ data: {
+ /** Format: email */
+ email: string;
+ reason?: string;
+ valid: boolean;
+ } & {
+ [key: string]: unknown;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A user-managed outbound webhook. Never carries a secret. */
+ Webhook: {
+ consecutiveFailures: number;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ disabledAt?: string | null;
+ /** @description Sending domains this endpoint is scoped to. Empty means every domain on the project. */
+ domains: string[];
+ eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uuid */
+ id: string;
+ /**
+ * Format: date-time
+ * @description While a rotation is in flight, when the OLD secret stops being accepted. `null` outside a rotation.
+ */
+ previousSecretExpiresAt?: string | null;
+ /** Format: uuid */
+ projectId: string;
+ /** @enum {string} */
+ status: "ACTIVE" | "PAUSED" | "DISABLED";
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ updatedAt: string;
+ /** Format: uri */
+ url: string;
+ };
+ /** @description An attempted webhook delivery. */
+ WebhookCall: {
+ attempt: number;
+ /**
+ * Format: date-time
+ * @description ISO 8601 datetime string
+ */
+ createdAt: string;
+ eventType: string;
+ /** Format: uuid */
+ id: string;
+ payload: {
+ [key: string]: unknown;
+ };
+ responseBody?: string | null;
+ responseStatus?: number | null;
+ /** @enum {string} */
+ status: "PENDING" | "SUCCESS" | "FAILED";
+ /** Format: uuid */
+ webhookId: string;
+ };
+ /** @description Cursor-paginated list of recent calls for a single webhook. */
+ WebhookCallsListResponse: {
+ cursor?: string | null;
+ data: components["schemas"]["WebhookCall"][];
+ hasMore?: boolean;
+ nextCursor?: string | null;
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely. */
+ WebhookCreateResponse: {
+ data: {
+ /** @description Plaintext shared secret. Returned ONCE on create. */
+ secret: string;
+ webhook: components["schemas"]["Webhook"];
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Single webhook (no secret). */
+ WebhookGetResponse: {
+ data: components["schemas"]["Webhook"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description List of webhooks for the auth'd project. */
+ WebhookListResponse: {
+ data: components["schemas"]["Webhook"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once. */
+ WebhookRotateSecretResponse: {
+ data: {
+ /** Format: uuid */
+ id: string;
+ /** @description New plaintext shared secret. */
+ secret: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ /** @description A webhook endpoint as exposed on the v1 API. The signing secret is NEVER on this shape — it is returned once, by create and by rotate, and no endpoint reads it back. */
+ WebhookV1: {
+ /** Format: date-time */
+ created_at: string;
+ event_types: string[];
+ /** Format: uuid */
+ id: string;
+ /** @enum {string} */
+ status: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: date-time */
+ updated_at: string;
+ url: string;
+ };
+ /** @description Body for POST /api/v1/webhooks. */
+ WebhookV1Create: {
+ event_types: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** Format: uri */
+ url: string;
+ };
+ /** @description A newly created webhook and its one-time signing secret. */
+ WebhookV1Created: {
+ /** @description The signing secret, shown EXACTLY ONCE. Store it now — no endpoint returns it again. */
+ secret: string;
+ webhook: components["schemas"]["WebhookV1"];
+ };
+ /** @description Acknowledgement that a webhook was deleted. */
+ WebhookV1Deleted: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Cursor-paginated list of webhook endpoints. */
+ WebhookV1List: {
+ data: components["schemas"]["WebhookV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description A freshly rotated signing secret, and the moment the outgoing one stops verifying. */
+ WebhookV1SecretRotated: {
+ /**
+ * Format: date-time
+ * @description When the PREVIOUS secret stops verifying. Until then every delivery carries both signatures, so a consumer can redeploy its verifier without dropping an event.
+ */
+ previous_secret_expires_at: string;
+ /** @description The new signing secret, shown EXACTLY ONCE. Store it now — no endpoint returns it again. */
+ secret: string;
+ };
+ /** @description Body for PATCH /api/v1/webhooks/{id}. */
+ WebhookV1Update: {
+ event_types?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
+ /** @enum {string} */
+ status?: "ACTIVE" | "PAUSED" | "DISABLED";
+ /** Format: uri */
+ url?: string;
+ };
+ /** @description Body for `POST /api/v1/workflows/{id}/clone`. */
+ WorkflowCloneV1: {
+ /** @description Name for the copy. Defaults to `Copy of `. */
+ name?: string;
+ };
+ /** @description Branches the run. Binary form: `field` + `operator` + `value`, whose outgoing transitions carry `{ "branch": "yes" }` / `{ "branch": "no" }`. Multi form: `mode: "multi"` + `field` + `branches`, whose transitions carry the branch id. */
+ WorkflowConditionStepV1: {
+ config: {
+ branches?: ({
+ id: string;
+ name: string;
+ /** @enum {string} */
+ operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists";
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ value?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ })[];
+ field?: string;
+ /** @enum {string} */
+ mode?: "multi";
+ /** @enum {string} */
+ operator?: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists";
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ value?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "CONDITION";
+ };
+ /** @description Body for POST /api/v1/workflows. */
+ WorkflowCreateV1: {
+ allow_reentry?: boolean;
+ description?: string;
+ /** @description Workflows are created disabled. A workflow can only be enabled once every step is configured. */
+ enabled?: boolean;
+ /** @description The custom event that starts this workflow, e.g. `user.signup`. Required for `EVENT` workflows (the default) and ignored for the other trigger types. */
+ event_name?: string;
+ /** @description For `SCHEDULE` workflows: how often the workflow fires, in milliseconds. Between one minute and 30 days; defaults to one hour. */
+ interval_ms?: number;
+ name: string;
+ /** @description Optional LINEAR chain to create the workflow with, in run order. The trigger step is prepended and each step is wired to the next, so this cannot express branches — use `PUT /api/v1/workflows/{id}/graph` for those. Omit it to create a workflow that holds only its trigger step. */
+ sequence?: components["schemas"]["WorkflowSequenceStepV1"][];
+ trigger_type?: components["schemas"]["WorkflowTriggerTypeV1"];
+ };
+ /** @description Pauses the run for `amount` × `unit`, up to 365 days. */
+ WorkflowDelayStepV1: {
+ config: {
+ amount?: number;
+ /** @enum {string} */
+ unit?: "minutes" | "hours" | "days";
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "DELAY";
+ };
+ /** @description Confirmation that a workflow was deleted. */
+ WorkflowDeletedV1: {
+ /** @enum {boolean} */
+ deleted: true;
+ /** Format: uuid */
+ id: string;
+ };
+ /** @description Body for POST /api/v1/workflows/{id}/executions. */
+ WorkflowExecutionStartV1: {
+ /**
+ * Format: uuid
+ * @description Contact to enter the workflow. Must belong to this project.
+ */
+ contact_id: string;
+ /** @description Extra variables merged into the contact's data for this run. */
+ context?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ };
+ /** @description One contact's run through a workflow. */
+ WorkflowExecutionV1: {
+ /** Format: date-time */
+ completed_at: string | null;
+ /** Format: uuid */
+ contact_id: string;
+ /** Format: uuid */
+ current_step_id: string | null;
+ exit_reason: string | null;
+ /** Format: uuid */
+ id: string;
+ /** Format: date-time */
+ started_at: string;
+ /** @enum {string} */
+ status: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Cursor-paginated list of workflow executions, newest first. */
+ WorkflowExecutionV1List: {
+ data: components["schemas"]["WorkflowExecutionV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Ends the run early and stamps `exit_reason`. */
+ WorkflowExitStepV1: {
+ config: {
+ reason?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "EXIT";
+ };
+ /** @description Body for `PUT /api/v1/workflows/{id}/graph`. Replaces the whole graph: a step whose id you send is kept and updated, a fresh id is created, and a step you omit is deleted along with its run history. Refused with 409 while the workflow has running executions, because those runs are standing on the steps being replaced. */
+ WorkflowGraphReplaceV1: {
+ /** @description The complete step set. Exactly one must be a `TRIGGER`. */
+ steps: components["schemas"]["WorkflowStepV1"][];
+ /** @description The complete edge set. Every `from_step_id`/`to_step_id` must name a step in this same document, and a step may not point at itself. */
+ transitions: components["schemas"]["WorkflowTransitionV1"][];
+ };
+ /** @description A workflow's complete step graph. The response of `GET /api/v1/workflows/{id}/graph` is accepted verbatim by `PUT` on the same path. */
+ WorkflowGraphV1: {
+ steps: components["schemas"]["WorkflowStepReadV1"][];
+ transitions: components["schemas"]["WorkflowTransitionV1"][];
+ /** @description The workflow's version AFTER this read. Every structural change bumps it and writes a `workflow_versions` snapshot, so a changed number between two reads means somebody edited the graph. */
+ version: number;
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Like `SEND_EMAIL`, but held until this contact's historically best open hour, falling back to `fallbackHour` and never waiting longer than `maxDelayHours`. */
+ WorkflowSendAtOptimalTimeStepV1: {
+ config: {
+ fallbackHour?: number;
+ maxDelayHours?: number;
+ /** Format: uuid */
+ templateId?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "SEND_AT_OPTIMAL_TIME";
+ };
+ /** @description Sends one email to the contact. Give it either `template_id` (preferred) or an inline `subject` + `body`. */
+ WorkflowSendEmailStepV1: {
+ config: {
+ body?: string;
+ recipient?: {
+ /** Format: email */
+ customEmail?: string;
+ /** @enum {string} */
+ type: "CONTACT" | "CUSTOM";
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ subject?: string;
+ /** Format: uuid */
+ templateId?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "SEND_EMAIL";
+ };
+ /**
+ * @description A step kind that may appear in a linear `sequence`. `TRIGGER` is prepended by the server.
+ * @enum {string}
+ */
+ WorkflowSequenceStepTypeV1: "SEND_EMAIL" | "DELAY" | "WAIT_FOR_EVENT" | "CONDITION" | "EXIT" | "WEBHOOK" | "UPDATE_CONTACT" | "SEND_AT_OPTIMAL_TIME";
+ /** @description One step of a linear workflow sequence. */
+ WorkflowSequenceStepV1: {
+ /** @description Step configuration. Keys are camelCase — see `WorkflowStepV1` for the shape each step type expects. */
+ config: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /** @description Human-readable label, e.g. `Day 0: welcome`. */
+ name: string;
+ /**
+ * Format: uuid
+ * @description For `SEND_EMAIL`: a template in this project.
+ */
+ template_id?: string;
+ type: components["schemas"]["WorkflowSequenceStepTypeV1"];
+ };
+ /** @description The workflow after a pause or resume, with the number of runs the call stopped. */
+ WorkflowStateChangeV1: {
+ /** @description Runs stopped by this call. Always 0 for `resume`; on `pause` it is the number of `RUNNING`/`WAITING` executions that were cancelled, which is what makes pausing different from `PATCH { enabled: false }` (that only stops NEW runs starting). */
+ cancelled_executions: number;
+ workflow: components["schemas"]["WorkflowV1"];
+ };
+ /** @description Execution, email and conversion totals for one workflow. */
+ WorkflowStatsV1: {
+ avg_duration_ms: number | null;
+ /** @description Execution counts keyed by status; a status with no executions is absent. */
+ by_status: {
+ [key: string]: number;
+ };
+ /** @description Completed ÷ finished executions (0–1). Null until at least one execution has finished. */
+ completion_rate: number | null;
+ conversions: {
+ count: number;
+ event_name: string;
+ /** Format: uuid */
+ goal_id: string;
+ name: string;
+ }[];
+ emails: {
+ clicked: number;
+ opened: number;
+ sent: number;
+ };
+ enabled: boolean;
+ name: string;
+ /** @description Steps in the workflow's graph, trigger step included. */
+ step_count: number;
+ total: number;
+ trigger_type: components["schemas"]["WorkflowTriggerTypeV1"] & unknown;
+ /** Format: uuid */
+ workflow_id: string;
+ };
+ /** @description Where this step sits on the editor canvas. */
+ WorkflowStepPositionV1: {
+ x: number;
+ y: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /** @description One node of a workflow graph, as read. */
+ WorkflowStepReadV1: {
+ /** @description The step's configuration, exactly as stored. See `WorkflowStepV1` for the keys each step type uses; keys are camelCase. */
+ config: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /** @enum {string} */
+ type: "TRIGGER" | "SEND_EMAIL" | "DELAY" | "WAIT_FOR_EVENT" | "CONDITION" | "EXIT" | "WEBHOOK" | "UPDATE_CONTACT" | "SEND_AT_OPTIMAL_TIME";
+ };
+ /** @description One node of a workflow graph. */
+ WorkflowStepV1: components["schemas"]["WorkflowTriggerStepV1"] | components["schemas"]["WorkflowSendEmailStepV1"] | components["schemas"]["WorkflowDelayStepV1"] | components["schemas"]["WorkflowWaitForEventStepV1"] | components["schemas"]["WorkflowConditionStepV1"] | components["schemas"]["WorkflowExitStepV1"] | components["schemas"]["WorkflowWebhookStepV1"] | components["schemas"]["WorkflowUpdateContactStepV1"] | components["schemas"]["WorkflowSendAtOptimalTimeStepV1"];
+ /** @description One directed edge between two steps. */
+ WorkflowTransitionV1: {
+ /** @description Null to always follow this edge. From a `CONDITION` step, `{ "branch": "yes" }`, `{ "branch": "no" }`, or `{ "branch": "" }` in the multi form. */
+ condition: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ /** Format: uuid */
+ from_step_id: string;
+ /**
+ * Format: uuid
+ * @description Caller-chosen on a write, exactly like a step id.
+ */
+ id: string;
+ /** @description Evaluation order among the edges leaving one step; lowest first. */
+ priority: number;
+ /** Format: uuid */
+ to_step_id: string;
+ };
+ /** @description The graph's single entry node. Its config mirrors the workflow's own trigger: `eventName` for `EVENT`, `intervalMs` for `SCHEDULE`, empty for `MANUAL`. */
+ WorkflowTriggerStepV1: {
+ config: {
+ eventName?: string;
+ intervalMs?: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "TRIGGER";
+ };
+ /**
+ * @description What starts this workflow. Defaults to `EVENT`, which is the only kind the API created before this field existed. `EVENT` requires `event_name`; `SCHEDULE` uses `interval_ms`; `MANUAL` is started only by `POST /api/v1/workflows/{id}/executions`.
+ * @enum {string}
+ */
+ WorkflowTriggerTypeV1: "EVENT" | "MANUAL" | "SCHEDULE";
+ /** @description Writes `updates` onto the contact, and optionally flips `subscribed`. */
+ WorkflowUpdateContactStepV1: {
+ config: {
+ subscribed?: boolean;
+ updates?: {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "UPDATE_CONTACT";
+ };
+ /** @description Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409. */
+ WorkflowUpdateV1: {
+ allow_reentry?: boolean;
+ description?: string;
+ enabled?: boolean;
+ event_name?: string;
+ /** @description For `SCHEDULE` workflows: how often the workflow fires, in milliseconds. Between one minute and 30 days; defaults to one hour. */
+ interval_ms?: number;
+ /** @description Per-workflow start rate cap. `null` removes the cap. */
+ max_executions_per_hour?: number | null;
+ name?: string;
+ /** @description Optional LINEAR chain that REPLACES every non-trigger step, in run order. Destructive: the existing steps are discarded and their ids do not survive, which is why omitting this field leaves the graph untouched. Branches need `PUT /api/v1/workflows/{id}/graph` instead. */
+ sequence?: components["schemas"]["WorkflowSequenceStepV1"][];
+ trigger_type?: components["schemas"]["WorkflowTriggerTypeV1"] & unknown;
+ };
+ /** @description An automation workflow as exposed on the v1 API. */
+ WorkflowV1: {
+ allow_reentry: boolean;
+ /** Format: date-time */
+ created_at: string;
+ description: string | null;
+ enabled: boolean;
+ /** @description Trigger event for `EVENT` workflows; null for the other trigger types. */
+ event_name: string | null;
+ /** Format: uuid */
+ id: string;
+ max_executions_per_hour: number | null;
+ name: string;
+ /** @description Steps in this workflow's graph, trigger step included — so a workflow that has only ever been created reports 1. Read `/graph` for the steps themselves. */
+ step_count: number;
+ /** @enum {string} */
+ trigger_type: "EVENT" | "MANUAL" | "SCHEDULE";
+ /** Format: date-time */
+ updated_at: string;
+ /** @description Incremented on every structural (step/transition) change. */
+ version: number;
+ };
+ /** @description Cursor-paginated list of workflows. */
+ WorkflowV1List: {
+ data: components["schemas"]["WorkflowV1"][];
+ has_more: boolean;
+ /** @description Pass as `after` to fetch the next page. `null` on the last page. */
+ next_cursor: string | null;
+ };
+ /** @description Parks the run until `eventName` is recorded for this contact, or `timeout` seconds pass. */
+ WorkflowWaitForEventStepV1: {
+ config: {
+ eventName?: string;
+ timeout?: number;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "WAIT_FOR_EVENT";
+ };
+ /** @description Calls an external URL. `url`, header values and the JSON body's string leaves are `{{variable}}`-interpolated from the contact and the run's variables. */
+ WorkflowWebhookStepV1: {
+ config: {
+ /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
+ body?: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
+ /** Format: uri */
+ url?: string;
+ } & {
+ [key: string]: string | number | boolean | {
+ [key: string]: unknown;
+ } | unknown[] | null;
+ };
+ /**
+ * Format: uuid
+ * @description Stable step id. On a graph write it is CHOSEN BY THE CALLER: send back the id you read to keep a step (and its run history), a fresh uuid to add one, and omit an id to delete that step.
+ */
+ id: string;
+ name: string;
+ position: components["schemas"]["WorkflowStepPositionV1"];
+ /**
+ * Format: uuid
+ * @description The `SEND_EMAIL`/`SEND_AT_OPTIMAL_TIME` template, as a relation. Null for every other step type.
+ */
+ template_id?: string | null;
+ /**
+ * @description discriminator enum property added by openapi-typescript
+ * @enum {string}
+ */
+ type: "WEBHOOK";
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+interface operations {
+ listContacts: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ subscribed?: "true" | "false";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ContactListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateContact"];
+ };
+ };
+ responses: {
+ /** @description Contact created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Email already exists for this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ bulkCreateContacts: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactBulkCreateBody"];
+ };
+ };
+ responses: {
+ /** @description Bulk-create result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ created: number;
+ errors: {
+ index: number;
+ message: string;
+ }[];
+ skipped: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ bulkDeleteContacts: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactBulkDeleteBody"];
+ };
+ };
+ responses: {
+ /** @description Bulk-delete result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ deleted: number;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ upsertContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateContact"];
+ };
+ };
+ responses: {
+ /** @description Contact created or updated */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Contact deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ updateContact: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateContactBody"];
+ };
+ };
+ responses: {
+ /** @description Updated contact */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Contact"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listDomains: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["DomainListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ addDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AddDomainBody"];
+ };
+ };
+ responses: {
+ /** @description Domain added */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault — retrying it unchanged will not help. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Domain removed */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuccessEmpty"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ assignDomainStream: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AssignDomainStream"];
+ };
+ };
+ responses: {
+ /** @description Updated sending identity */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Domain"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ startDomainSetup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Guided setup session */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /**
+ * Format: uri
+ * @description Open this in a browser to publish the records. Short-lived and domain-specific.
+ */
+ connectUrl: string;
+ /** @description When `connectUrl` stops working. */
+ expiresAt: string;
+ token: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getDomainVerification: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Verification status */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["DomainVerificationStatus"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ verifyDomain: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Verification status */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["DomainVerificationStatus"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listEmails: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ tag?: string;
+ /** @description Delivery lifecycle of the message. Engagement is reported separately. */
+ status?: components["schemas"]["EmailDeliveryStatus"];
+ from?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendEmail: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendEmail"];
+ };
+ };
+ responses: {
+ /** @description Email accepted / sent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SendEmailResponse"];
+ };
+ };
+ /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendEmailBatch: {
+ parameters: {
+ query?: never;
+ header?: {
+ "Idempotency-Key"?: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchSendBody"];
+ };
+ };
+ responses: {
+ /** @description All entries sent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchSendResponse"];
+ };
+ };
+ /** @description Partial success — at least one entry failed */
+ 207: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchSendResponse"];
+ };
+ };
+ /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email and its delivery history */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailDetailResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ cancelScheduledEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email cancelled */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["EmailResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Email already past PENDING */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ subscribeToList: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description List id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListSubscribe"];
+ };
+ };
+ responses: {
+ /** @description Contact subscribed, or an existing membership returned unchanged */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ListSubscribeResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ unsubscribeFromList: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description List id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListUnsubscribe"];
+ };
+ };
+ responses: {
+ /** @description Contact unsubscribed */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ListUnsubscribeResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listMailboxes: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Mailbox"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateMailboxBody"];
+ };
+ };
+ responses: {
+ /** @description Mailbox provisioned */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Mailbox"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The address already exists, the domain is not verified, or the project is at its 10-mailbox limit. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Provisioning failed in the mail server. The mailbox row is left `FAILED` and can be retried. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox with connection settings */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["MailboxDetail"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteMailbox: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mailbox deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @enum {boolean} */
+ deleted: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listAppPasswords: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description App password list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["AppPassword"][];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createAppPassword: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateAppPassword"];
+ };
+ };
+ responses: {
+ /** @description App password created; the secret is behind the one-time link */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["AppPasswordReveal"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ revokeAppPassword: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ passwordId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description App password revoked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @enum {boolean} */
+ revoked: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ draftMailboxMessage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["DraftMailboxMessage"];
+ };
+ };
+ responses: {
+ /** @description A draft. Nothing was sent. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /** @description Suggested plain-text body, or null. */
+ body: string | null;
+ /**
+ * @description Always false. Reported rather than assumed, so a draft cannot be mistaken for a send.
+ * @enum {boolean}
+ */
+ sent: false;
+ /** @description Suggested subject, or null. */
+ subject: string | null;
+ /** @description Alternative subject lines (`subject` mode); empty otherwise. */
+ subjects: string[];
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The drafting model was unreachable or returned nothing usable. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ sendMailboxMessage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ComposeMailboxMessage"];
+ };
+ };
+ responses: {
+ /** @description Message submitted */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ /**
+ * Format: uuid
+ * @description The conversation this send started. Replies thread onto it.
+ */
+ conversationId: string;
+ /**
+ * Format: uuid
+ * @description The stored outbound message.
+ */
+ messageId: string;
+ /** @enum {boolean} */
+ submitted: true;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The mailbox is not active, a recipient is suppressed (`RECIPIENT_SUPPRESSED`), or the content scanner refused the message (`CONTENT_REFUSED`). */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description The mail server refused the submission. Nothing was sent. */
+ 502: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Message screening could not reach a verdict. Nothing was sent; retry shortly. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listApiKeys: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ApiKeyListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateApiKeyBody"];
+ };
+ };
+ responses: {
+ /** @description API key created; the secret is behind the reveal link. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
+ data: components["schemas"]["ApiKey"] & {
+ /**
+ * Format: date-time
+ * @description When the reveal link stops working. Create or rotate again to get a new one.
+ */
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
+ */
+ revealUrl: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ revokeApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ /** @description API key id. */
+ keyId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key revoked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuccessEmpty"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ rotateApiKey: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project id. */
+ id: string;
+ /** @description API key id. */
+ keyId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description API key rotated; the new secret is behind the reveal link. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: {
+ lastFour: string;
+ /**
+ * Format: date-time
+ * @description When the reveal link stops working. Create or rotate again to get a new one.
+ */
+ revealExpiresAt: string;
+ /**
+ * Format: uri
+ * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
+ */
+ revealUrl: string;
+ };
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listSnippets: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SnippetListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateSnippet"];
+ };
+ };
+ responses: {
+ /** @description Snippet created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description A snippet with that name already exists in this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deleteSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Snippet deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ updateSnippet: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSnippet"];
+ };
+ };
+ responses: {
+ /** @description Updated snippet */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Snippet"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description A snippet with that name already exists in this project */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listSuppressions: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuppressionListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ addSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AddSuppression"];
+ };
+ };
+ responses: {
+ /** @description Suppression added */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Suppression"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ checkSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description URL-encoded email address */
+ email: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression check result */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SuppressionCheckResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ removeSuppression: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description URL-encoded email address */
+ email: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Suppression removed */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listTemplates: {
+ parameters: {
+ query?: {
+ limit?: number;
+ cursor?: string;
+ search?: string;
+ emailCategory?: "MARKETING" | "TRANSACTIONAL" | "SELF_MANAGED_UNSUBSCRIBE";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Template list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TemplateListResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ createTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateTemplate"];
+ };
+ };
+ responses: {
+ /** @description Template created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** @enum {boolean} */
- success?: false;
- };
- /** @description Every distinct event name in the project, most frequent first. */
- EventNamesV1: {
- data: string[];
- };
- /** @description Per-name event counts over the applied window. */
- EventStatsV1: {
- data: {
- count: number;
- name: string;
- }[];
- window: components["schemas"]["AnalyticsWindowV1"];
- };
- /** @description Body for POST /api/v1/events. */
- EventTrackV1: {
- /**
- * Format: uuid
- * @description Contact the event belongs to. Must already exist in this project — unlike the legacy `POST /api/track`, this endpoint never creates contacts. Omit for a project-level event.
- */
- contact_id?: string;
- /** @description Arbitrary event payload. */
- data?: {
- [key: string]: string | number | boolean | {
- [key: string]: unknown;
- } | unknown[] | null;
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** @description Event name, e.g. `user.signup`. */
- name: string;
- };
- /** @description A recorded custom event. */
- EventV1: {
- /** Format: uuid */
- contact_id: string | null;
- /** Format: date-time */
- created_at: string;
- /** @description The payload recorded with the event, or null. */
- data: {
- [key: string]: unknown;
- } | null;
- /** Format: uuid */
- email_id: string | null;
- /** Format: uuid */
- id: string;
- name: string;
- };
- /** @description Cursor-paginated list of events, newest first. */
- EventV1List: {
- data: components["schemas"]["EventV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
};
- /** @description A filter condition: one or more groups combined with `logic`. */
- FilterConditionV1: {
- groups: components["schemas"]["FilterGroupV1"][];
- /** @enum {string} */
- logic: "AND" | "OR";
+ };
+ getTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
};
- /** @description A group of filters. The filters inside one group ALWAYS combine with AND; `conditions` nests a further condition under this group, which is how OR-of-ANDs (and deeper) is expressed. */
- FilterGroupV1: {
- conditions?: components["schemas"]["FilterConditionV1"];
- filters: components["schemas"]["SegmentFilterV1"][];
+ requestBody?: never;
+ responses: {
+ /** @description Template */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Success envelope carrying the affected resource's id, e.g. after a delete. */
- IdResponse: {
- data: {
- /** Format: uuid */
+ };
+ deleteTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
id: string;
};
- /** @enum {boolean} */
- success: true;
+ cookie?: never;
};
- /** @description Body for POST /api/lists/{id}/subscribe. */
- ListSubscribe: {
- /**
- * @description Permission to reverse an earlier opt-out. When the email already has an UNSUBSCRIBED membership on this list, the call fails with 409 RESUBSCRIBE_CONFIRMATION_REQUIRED unless this is `true`. Send `true` only when the contact is acting for themselves — a public subscribe form they submitted is affirmative consent — never for an operator-initiated add.
- * @default false
- */
- allowResubscribe: boolean;
- /** @description Custom fields to upsert onto the contact as part of subscribing. */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Template deleted */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["IdResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Template still in use */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- /** Format: email */
- email: string;
};
- /** @description Result of a list-subscribe call. */
- ListSubscribeResponse: {
- data: {
- /** @description Present only when the list has doubleOptIn enabled. Sendly does not send the confirmation email — deliver /api/lists/confirm?token= to the contact. Valid for 24 hours. */
- confirmToken?: string;
- /** @description True when the membership row did not exist before this call. */
- created: boolean;
- /** Format: uuid */
- membershipId: string;
- /**
- * @description Status the membership held before this call; null when it did not exist. Use this rather than `created` to describe the transition to the user.
- * @enum {string|null}
- */
- previousStatus: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED" | null;
- /** @enum {string} */
- status: "PENDING" | "CONFIRMED" | "UNSUBSCRIBED";
+ };
+ updateTemplate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/lists/{id}/unsubscribe. */
- ListUnsubscribe: {
- /** Format: email */
- email: string;
+ cookie?: never;
};
- /** @description Echoes the address that was unsubscribed. */
- ListUnsubscribeResponse: {
- data: {
- /** Format: email */
- email: string;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateTemplate"];
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description A receiving mailbox on one of the project's verified domains. */
- Mailbox: {
- /**
- * Format: email
- * @description The full mailbox address, e.g. `support@superbooks.io`.
- */
- address: string;
- /** Format: date-time */
- createdAt: string;
- displayName: string | null;
- /**
- * Format: uuid
- * @description The verified domain this mailbox lives on.
- */
- domainId: string;
- /** Format: uuid */
- id: string;
- /** @description Always null. Mailbox quotas are not implemented — the value was never applied to the mail account — so this field reports the absence rather than a number nothing enforces. */
- quotaBytes: number | null;
- /**
- * @description `PROVISIONING` while the mail account is being created, `ACTIVE` once it can receive, `SUSPENDED` when receiving is paused, `FAILED` when provisioning did not complete. A `FAILED` mailbox can be re-created with the same address — the retry reclaims the row.
- * @enum {string}
- */
- status: "PROVISIONING" | "ACTIVE" | "SUSPENDED" | "FAILED";
};
- /** @description A mailbox plus its IMAP/SMTP connection settings. */
- MailboxDetail: components["schemas"]["Mailbox"] & {
- /** @description Host, port and username for connecting a mail client. The PASSWORD is not here and is never returned by this endpoint — create an app password for that. */
- settings: {
- imap: {
- host: string;
- port: number;
- /** @description Transport security, e.g. `SSL/TLS`. */
- security: string;
- /** @description The mailbox address — it is also the login. */
- username: string;
+ responses: {
+ /** @description Updated template */
+ 200: {
+ headers: {
+ [name: string]: unknown;
};
- smtp: {
- host: string;
- port: number;
- /** @description Transport security, e.g. `SSL/TLS`. */
- security: string;
- /** @description The mailbox address — it is also the login. */
- username: string;
+ content: {
+ "application/json": {
+ data: components["schemas"]["Template"];
+ /** @enum {boolean} */
+ success: true;
+ };
};
};
- };
- /** @description RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface. */
- Problem: {
- /** @description Machine-readable lowercase error code, e.g. `scope_missing`. */
- code: string;
- /** @description Explanation specific to this occurrence. */
- detail?: string;
- /** @description Field-level failures. Present on 422 `validation_error` responses. */
- errors?: {
- code: string;
- message: string;
- /** @description RFC 6901 JSON Pointer to the offending field. */
- pointer: string;
- }[];
- /** @description Request path the failure occurred on. */
- instance?: string;
- /** @description Correlation id — quote it in support requests. */
- request_id?: string;
- /** @description HTTP status code, repeated in the body. */
- status: number;
- /** @description Short, stable summary — the same for every occurrence of a `type`. */
- title: string;
- /**
- * Format: uri
- * @description Dereferenceable URI identifying the error class, anchored on the docs errors page.
- */
- type: string;
- };
- ProjectRecord: {
- billingLimitCampaigns: number | null;
- billingLimitInbound: number | null;
- billingLimitTransactional: number | null;
- billingLimitWorkflows: number | null;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- disabled: boolean;
- disabledReason: string | null;
- /** Format: uuid */
- id: string;
- /** @description ISO 639-1 code for customer-facing content. */
- language: string;
- name: string;
- organizationId: string | null;
- /** @description Local-part of the sandbox quick-start sender; null until first derived. */
- sandboxHandle: string | null;
- sesRegion: string | null;
- stripeCustomerId: string | null;
- stripeSubscriptionId: string | null;
- /** @enum {string} */
- tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- };
- /** @description The project the presented credential is scoped to. */
- ProjectV1: {
- /** Format: date-time */
- created_at: string;
- /** @description A disabled project sends nothing; every send is refused. */
- disabled: boolean;
- /** Format: uuid */
- id: string;
- /** @description ISO 639-1 code for customer-facing content. */
- language: string;
- name: string;
- /** @description This project's quick-start sender, usable with no domain setup — but only to the project owner's own verified address, and under a daily cap. Null when none can be derived. */
- sandbox_address: string | null;
- /** @description Locked once the first domain is added. */
- ses_region: string | null;
- /** @enum {string} */
- tracking: "ENABLED" | "DISABLED" | "MARKETING_ONLY";
- };
- /** @description A contact belonging to a segment. */
- SegmentContactV1: {
- /** Format: date-time */
- created_at: string;
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- custom_fields: {
- [key: string]: unknown;
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
};
- email: string;
- /** Format: uuid */
- id: string;
- subscribed: boolean;
};
- /** @description Cursor-paginated list of the contacts belonging to a segment. */
- SegmentContactV1List: {
- data: components["schemas"]["SegmentContactV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ };
+ trackEvent: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description One comparison. `field` addresses a contact column (`email`, `createdAt`, …) or a `customFields.` path. `value` is whatever the operator compares against — its JSON type follows the field, and it is omitted entirely for the presence operators (`exists`, `notExists`). `unit` applies only to the relative-time operators (`within`, `olderThan`, and the `triggered*` family). */
- SegmentFilterV1: {
- field: string;
- /** @enum {string} */
- operator: "equals" | "notEquals" | "contains" | "notContains" | "greaterThan" | "lessThan" | "greaterThanOrEqual" | "lessThanOrEqual" | "exists" | "notExists" | "within" | "olderThan" | "triggered" | "triggeredWithin" | "triggeredOlderThan" | "notTriggered" | "notTriggeredWithin" | "isMemberOf";
- /** @enum {string} */
- unit?: "days" | "hours" | "minutes";
- value?: unknown;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TrackEvent"];
+ };
};
- /** @description A segment as exposed on the v1 API. */
- SegmentV1: {
- condition: components["schemas"]["FilterConditionV1"] | null;
- /** Format: date-time */
- created_at: string;
- description: string | null;
- /** Format: uuid */
- id: string;
- member_count: number;
- name: string;
- track_membership: boolean;
- /** @enum {string} */
- type: "DYNAMIC" | "STATIC";
- /** Format: date-time */
- updated_at: string;
+ responses: {
+ /** @description Event tracked */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TrackEventResponse"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Body for POST /api/v1/segments. `condition` is required when `type` is `DYNAMIC`. */
- SegmentV1Create: {
- condition?: components["schemas"]["FilterConditionV1"];
- description?: string;
- name: string;
- /**
- * @description Emit segment entry/exit events as contacts move in and out. Off by default — it costs a membership write per transition.
- * @default false
- */
- track_membership: boolean;
- /**
- * @default DYNAMIC
- * @enum {string}
- */
- type: "DYNAMIC" | "STATIC";
+ };
+ createProject: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Acknowledgement that a segment was deleted. */
- SegmentV1Deleted: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ requestBody: {
+ content: {
+ "application/json": {
+ name: string;
+ /**
+ * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
+ * @enum {string}
+ */
+ sesRegion?: "us-east-1" | "us-west-2" | "eu-west-1";
+ };
+ };
};
- /** @description Cursor-paginated list of segments. */
- SegmentV1List: {
- data: components["schemas"]["SegmentV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
+ responses: {
+ /** @description Project created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ProjectRecord"];
+ };
+ };
+ /** @description Validation error */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unauthorized — missing or invalid auth */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Forbidden — insufficient permissions or project disabled */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Validation failed — request body or query parameters did not match the schema */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit or billing limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
};
- /** @description Body for PATCH /api/v1/segments/{id}. `type` is deliberately absent — it is fixed at creation. `condition` is ignored on a STATIC segment. */
- SegmentV1Update: {
- condition?: components["schemas"]["FilterConditionV1"];
- description?: string;
- name?: string;
- track_membership?: boolean;
+ };
+ v1GetCampaignAnalytics: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/emails — send a single transactional email. Either `template` or `subject`+`body` is required. */
- SendEmail: {
- attachments?: {
- content: string;
- contentId?: string;
- contentType: string;
- /**
- * @default attachment
- * @enum {string}
- */
- disposition: "attachment" | "inline";
- filename: string;
- }[];
- bcc?: string[];
- body?: string;
- cc?: string[];
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Campaign statistics */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsCampaignStatsV1"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- from?: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- headers?: {
- [key: string]: string;
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- name?: string;
- /** Format: email */
- reply?: string;
- subject?: string;
- subscribed?: boolean;
- tags?: string[];
- /** Format: uuid */
- template?: string;
- to: string | {
- /** Format: email */
- email: string;
- name?: string;
- } | (string | {
- /** Format: email */
- email: string;
- name?: string;
- })[];
- };
- /** @description Result of a send (`executeSendEmail`): one `emails` entry per recipient — a single request with an array `to` fans out to several — plus the send `timestamp`. */
- SendEmailData: {
- emails: components["schemas"]["SendEmailRecipientResult"][];
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- timestamp: string;
- };
- /** @description Per-recipient result: the upserted `contact` (id + email) and `email` — the id of the queued email record for that recipient. */
- SendEmailRecipientResult: {
- contact: {
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** Format: uuid */
- email: string;
};
- /** @description Successful response for `POST /api/emails`. `data.emails[i].email` is the queued email id for recipient `i`; poll `GET /api/emails/{id}` for its delivery status. */
- SendEmailResponse: {
- data: components["schemas"]["SendEmailData"];
- /** @enum {boolean} */
- success: true;
+ };
+ v1GetAnalyticsTimeseries: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/emails. Either `template` or `subject`+`body` is required, and `to` names exactly one recipient. */
- SendEmailV1: {
- attachments?: {
- content: string;
- contentId?: string;
- contentType: string;
- /**
- * @default attachment
- * @enum {string}
- */
- disposition: "attachment" | "inline";
- filename: string;
- }[];
- bcc?: string[];
- body?: string;
- cc?: string[];
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Daily time series */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsTimeseriesV1"];
+ };
};
- from?: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- headers?: {
- [key: string]: string;
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- name?: string;
- /** Format: email */
- reply?: string;
- subject?: string;
- subscribed?: boolean;
- tags?: string[];
- /** Format: uuid */
- template?: string;
- /** @description The single recipient. Use `cc`/`bcc` to copy others on the same message. */
- to: string | {
- /** Format: email */
- email: string;
- name?: string;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- };
- /** @description Body for POST /api/v1/emails/test. `subject` and `body` are required; `to` defaults to the project owner's verified email, and `from` is refused. */
- SendTestEmailV1: {
- /** @description HTML body. Merge tags are rendered as on any other send. */
- body: string;
- /** @description NOT ACCEPTED. The sender is always this project's sandbox address, resolved server-side; naming one here is refused rather than ignored, so a request that expects a different sender never gets a success it would misread. Read `sandbox_address` from `GET /api/v1/projects` to learn the address, or `from` off this response. */
- from?: string;
- subject: string;
- /**
- * Format: email
- * @description Where to send it. Optional — it defaults to the project owner's own verified account email, which is the only address a sandbox send may reach. Any other value is refused.
- */
- to?: string;
- };
- /** @description Bare success envelope with no payload. */
- SuccessEmpty: {
- /** @enum {boolean} */
- success: true;
- };
- /** @description A single suppressed-email record. */
- Suppression: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /** Format: email */
- email: string;
- /** Format: uuid */
- id: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- reason: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- /** @enum {string} */
- source: "SES_WEBHOOK" | "API" | "DASHBOARD";
- };
- /** @description Result of GET /api/suppression/{email} — whether the address is suppressed. */
- SuppressionCheckResponse: {
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt?: string;
- /** @enum {string} */
- reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- /** @enum {string} */
- source?: "SES_WEBHOOK" | "API" | "DASHBOARD";
- suppressed: boolean;
- };
- /** @description Cursor-paginated list of suppressions. */
- SuppressionListResponse: {
- cursor?: string | null;
- data: components["schemas"]["Suppression"][];
- hasMore?: boolean;
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
- };
- /** @description A reusable email template. */
- Template: {
- body: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- description?: string | null;
- /** Format: email */
- from: string;
- fromName?: string | null;
- /** Format: uuid */
- id: string;
- name: string;
- /** Format: uuid */
- projectId: string;
- /** Format: email */
- replyTo?: string | null;
- subject: string;
- /** @enum {string} */
- type: "MARKETING" | "TRANSACTIONAL" | "HEADLESS";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- };
- /** @description Cursor-paginated list of templates. */
- TemplateListResponse: {
- data: {
- /** @description Cursor for the next page; omitted on the last page. */
- cursor?: string;
- data: components["schemas"]["Template"][];
- hasMore: boolean;
- total: number;
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/track — record a custom event for a contact. */
- TrackEvent: {
- /** @description Arbitrary JSON value (string, number, boolean, null, array, or object). */
- data?: {
- [key: string]: unknown;
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** Format: email */
- email: string;
- event: string;
- subscribed?: boolean;
};
- /** @description Response from POST /api/track. */
- TrackEventResponse: {
- data: {
- /** Format: uuid */
- contact: string;
- /** Format: uuid */
- event: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- timestamp: string;
+ };
+ v1ListTopCampaigns: {
+ parameters: {
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
+ limit?: number;
};
- /** @enum {boolean} */
- success: true;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description Body for PATCH /api/contacts/{id}. `email` is immutable here — use upsert to change addresses. */
- UpdateContactBody: {
- customFields?: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Ranked campaigns */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AnalyticsTopCampaignsV1"];
+ };
};
- subscribed?: boolean;
- };
- /** @description Body for PATCH /api/templates/{id}. */
- UpdateTemplate: {
- body?: string;
- description?: string;
- /** Format: email */
- from?: string;
- fromName?: string | null;
- name?: string;
- /** Format: email */
- replyTo?: string | null;
- subject?: string;
- /** @enum {string} */
- type?: "TRANSACTIONAL" | "MARKETING" | "HEADLESS";
- };
- /** @description Body for PATCH /api/webhooks/{id}. */
- UpdateWebhook: {
- eventTypes?: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** @enum {string} */
- status?: "ACTIVE" | "PAUSED" | "DISABLED";
- /** Format: uri */
- url?: string;
- };
- /** @description Current email usage against the limits that are actually enforced. */
- UsageV1: {
- daily: {
- /** @description Today's sends. Null when the counter could not be read. */
- emails_sent: number | null;
- limit: number;
- /** @enum {string} */
- trust_tier: "NEW" | "ESTABLISHED" | "TRUSTED";
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- monthly: {
- categories: {
- campaign: {
- emails_sent: number;
- limit: number | null;
- };
- inbound: {
- emails_sent: number;
- limit: number | null;
- };
- transactional: {
- emails_sent: number;
- limit: number | null;
- };
- workflow: {
- emails_sent: number;
- limit: number | null;
- };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
- emails_sent: number;
- /** @description Monthly cap on the total. Null when per-category limits govern instead. */
- limit: number | null;
};
- /**
- * @description `custom` when an operator set per-category limits, `pro` on an active subscription or store entitlement, else `free`.
- * @enum {string}
- */
- plan: "free" | "pro" | "custom";
- };
- /** @description Body for POST /api/verify — validate email syntax, MX, disposable, etc. */
- VerifyEmail: {
- /** Format: email */
- email: string;
- };
- /** @description Response from POST /api/verify — outcome of the syntax/MX/disposable check. */
- VerifyEmailResponse: {
- data: {
- /** Format: email */
- email: string;
- reason?: string;
- valid: boolean;
- } & {
- [key: string]: unknown;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @enum {boolean} */
- success: true;
};
- /** @description A user-managed outbound webhook. */
- Webhook: {
- consecutiveFailures: number;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- disabledAt?: string | null;
- eventTypes: ("email.sent" | "email.delivered" | "email.opened" | "email.clicked" | "email.bounced" | "email.complained" | "email.failed" | "contact.created" | "contact.unsubscribed" | "contacts.bulk_created")[];
- /** Format: uuid */
- id: string;
- lastFour?: string;
- /** Format: uuid */
- projectId: string;
- /** @enum {string} */
- status: "ACTIVE" | "PAUSED" | "DISABLED";
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- updatedAt: string;
- /** Format: uri */
- url: string;
+ };
+ v1ListCampaigns: {
+ parameters: {
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- /** @description An attempted webhook delivery. */
- WebhookCall: {
- attempt: number;
- /**
- * Format: date-time
- * @description ISO 8601 datetime string
- */
- createdAt: string;
- eventType: string;
- /** Format: uuid */
- id: string;
- payload: {
- [key: string]: unknown;
+ requestBody?: never;
+ responses: {
+ /** @description Campaign list */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1List"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- responseBody?: string | null;
- responseStatus?: number | null;
- /** @enum {string} */
- status: "PENDING" | "SUCCESS" | "FAILED";
- /** Format: uuid */
- webhookId: string;
- };
- /** @description Cursor-paginated list of recent calls for a single webhook. */
- WebhookCallsListResponse: {
- cursor?: string | null;
- data: components["schemas"]["WebhookCall"][];
- hasMore?: boolean;
- nextCursor?: string | null;
- /** @enum {boolean} */
- success: true;
};
- /** @description Result of POST /api/webhooks. `secret` is the only time the plaintext is returned — store it securely. */
- WebhookCreateResponse: {
- /** @description A user-managed outbound webhook. */
- data: components["schemas"]["Webhook"] & {
- /** @description Plaintext shared secret. Returned ONCE on create. */
- secret: string;
+ };
+ v1CreateCampaign: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
};
- /** @enum {boolean} */
- success: true;
+ path?: never;
+ cookie?: never;
};
- /** @description Single webhook (no secret). */
- WebhookGetResponse: {
- data: components["schemas"]["Webhook"];
- /** @enum {boolean} */
- success: true;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CampaignV1Create"];
+ };
};
- /** @description List of webhooks for the auth'd project. */
- WebhookListResponse: {
- data: components["schemas"]["Webhook"][];
- /** @enum {boolean} */
- success: true;
+ responses: {
+ /** @description Campaign created */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1"];
+ };
+ };
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `resource_not_found` — `segment_id` names a segment that does not belong to this project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
};
- /** @description Response from POST /api/webhooks/{id}/rotate-secret — returns the new plaintext once. */
- WebhookRotateSecretResponse: {
- data: {
- /** Format: uuid */
+ };
+ v1GetCampaign: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Resource id. */
id: string;
- /** @description New plaintext shared secret. */
- secret: string;
};
- /** @enum {boolean} */
- success: true;
- };
- /** @description Body for POST /api/v1/workflows. */
- WorkflowCreateV1: {
- allow_reentry?: boolean;
- description?: string;
- /** @description Workflows are created disabled. A workflow can only be enabled once every step is configured. */
- enabled?: boolean;
- /** @description The custom event that starts this workflow, e.g. `user.signup`. */
- event_name: string;
- name: string;
- };
- /** @description Confirmation that a workflow was deleted. */
- WorkflowDeletedV1: {
- /** @enum {boolean} */
- deleted: true;
- /** Format: uuid */
- id: string;
+ cookie?: never;
};
- /** @description Body for POST /api/v1/workflows/{id}/executions. */
- WorkflowExecutionStartV1: {
- /**
- * Format: uuid
- * @description Contact to enter the workflow. Must belong to this project.
- */
- contact_id: string;
- /** @description Extra variables merged into the contact's data for this run. */
- context?: {
- [key: string]: string | number | boolean | {
- [key: string]: unknown;
- } | unknown[] | null;
+ requestBody?: never;
+ responses: {
+ /** @description The campaign */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CampaignV1"];
+ };
};
- };
- /** @description One contact's run through a workflow. */
- WorkflowExecutionV1: {
- /** Format: date-time */
- completed_at: string | null;
- /** Format: uuid */
- contact_id: string;
- /** Format: uuid */
- current_step_id: string | null;
- exit_reason: string | null;
- /** Format: uuid */
- id: string;
- /** Format: date-time */
- started_at: string;
- /** @enum {string} */
- status: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
- /** Format: uuid */
- workflow_id: string;
- };
- /** @description Cursor-paginated list of workflow executions, newest first. */
- WorkflowExecutionV1List: {
- data: components["schemas"]["WorkflowExecutionV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
- };
- /** @description Execution, email and conversion totals for one workflow. */
- WorkflowStatsV1: {
- avg_duration_ms: number | null;
- /** @description Execution counts keyed by status; a status with no executions is absent. */
- by_status: {
- [key: string]: number;
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- /** @description Completed ÷ finished executions (0–1). Null until at least one execution has finished. */
- completion_rate: number | null;
- conversions: {
- count: number;
- event_name: string;
- /** Format: uuid */
- goal_id: string;
- name: string;
- }[];
- emails: {
- clicked: number;
- opened: number;
- sent: number;
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `internal_error`. */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
};
- total: number;
- /** Format: uuid */
- workflow_id: string;
- };
- /** @description Body for PATCH /api/v1/workflows/{id}. Every field is optional; omitted fields are left unchanged. Changing the trigger while executions are running answers 409. */
- WorkflowUpdateV1: {
- allow_reentry?: boolean;
- description?: string;
- enabled?: boolean;
- event_name?: string;
- /** @description Per-workflow start rate cap. `null` removes the cap. */
- max_executions_per_hour?: number | null;
- name?: string;
- };
- /** @description An automation workflow as exposed on the v1 API. */
- WorkflowV1: {
- allow_reentry: boolean;
- /** Format: date-time */
- created_at: string;
- description: string | null;
- enabled: boolean;
- /** @description Trigger event for `EVENT` workflows; null for the other trigger types. */
- event_name: string | null;
- /** Format: uuid */
- id: string;
- max_executions_per_hour: number | null;
- name: string;
- /** @enum {string} */
- trigger_type: "EVENT" | "MANUAL" | "SCHEDULE";
- /** Format: date-time */
- updated_at: string;
- /** @description Incremented on every structural (step/transition) change. */
- version: number;
- };
- /** @description Cursor-paginated list of workflows. */
- WorkflowV1List: {
- data: components["schemas"]["WorkflowV1"][];
- has_more: boolean;
- /** @description Pass as `after` to fetch the next page. `null` on the last page. */
- next_cursor: string | null;
};
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-interface operations {
- listContacts: {
+ v1DeleteCampaign: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- search?: string;
- subscribed?: "true" | "false";
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact list */
+ /** @description Campaign deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ContactListResponse"];
+ "application/json": components["schemas"]["CampaignV1Deleted"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only `DRAFT` campaigns can be deleted. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createContact: {
+ v1UpdateCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateContact"];
+ "application/json": components["schemas"]["CampaignV1Update"];
};
};
responses: {
- /** @description Contact created */
- 201: {
+ /** @description The updated campaign */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — the campaign is not in an editable status, or the segment change is not allowed. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Email already exists for this project */
- 409: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- bulkCreateContacts: {
+ v1CancelCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ContactBulkCreateBody"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Bulk-create result */
+ /** @description The cancelled campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- created: number;
- errors: {
- index: number;
- message: string;
- }[];
- skipped: number;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- };
- };
- bulkDeleteContacts: {
- parameters: {
- query?: never;
+ };
+ };
+ v1ListCampaignFailures: {
+ parameters: {
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["ContactBulkDeleteBody"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Bulk-delete result */
+ /** @description Failed sends */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- deleted: number;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1FailureList"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- upsertContact: {
+ v1PauseCampaign: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateContact"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Contact created or updated */
+ /** @description The paused campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `SENDING` campaign can be paused. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getContact: {
+ v1ResumeCampaign: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact */
+ /** @description The resumed campaign */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `PAUSED` campaign can be resumed. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteContact: {
+ v1RetryCampaignFailures: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Contact deleted */
+ /** @description The retry was queued */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["IdResponse"];
+ "application/json": components["schemas"]["CampaignV1RetryFailed"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — only a `SENT` campaign can have its failed sends retried. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `conflict` — a retry is already running for this campaign. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- updateContact: {
+ v1SendCampaign: {
parameters: {
query?: never;
- header?: never;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
+ };
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
+ requestBody?: {
content: {
- "application/json": components["schemas"]["UpdateContactBody"];
+ "application/json": components["schemas"]["CampaignV1Send"];
};
};
responses: {
- /** @description Updated contact */
+ /** @description The campaign, now `SENDING` or `SCHEDULED` */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Contact"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["CampaignV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listDomains: {
+ v1GetCampaignStats: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Domain list */
+ /** @description Campaign statistics */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["DomainListResponse"];
+ "application/json": components["schemas"]["CampaignV1Stats"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- addDomain: {
+ v1ListContacts: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Case-insensitive substring match on the email address. */
+ search?: string;
+ /** @description Filter to subscribed (`true`) or unsubscribed (`false`) contacts. Omit for both. */
+ subscribed?: "true" | "false";
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["AddDomainBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Domain added */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- data: components["schemas"]["Domain"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description Contact list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["ContactV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault — retrying it unchanged will not help. */
- 502: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getDomain: {
+ v1CreateContact: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactV1Create"];
+ };
+ };
responses: {
- /** @description Domain */
- 200: {
+ /** @description The created contact */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Domain"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — a contact with this email already exists in this project. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteDomain: {
+ v1GetContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Domain removed */
+ /** @description The contact */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuccessEmpty"];
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- startDomainSetup: {
+ v1DeleteContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Guided setup session */
+ /** @description Contact deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /**
- * Format: uri
- * @description Open this in a browser to publish the records. Short-lived and domain-specific.
- */
- connectUrl: string;
- /** @description When `connectUrl` stops working. */
- expiresAt: string;
- token: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getDomainVerification: {
+ v1UpdateContact: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ContactV1Update"];
+ };
+ };
responses: {
- /** @description Verification status */
+ /** @description The updated contact */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["DomainVerificationStatus"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- verifyDomain: {
+ v1GetContactTopicPreferences: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Verification status */
+ /** @description The contact's preferences */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["DomainVerificationStatus"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ContactTopicPreferencesV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listEmails: {
+ v1DiagnoseDeliverability: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- tag?: string;
- status?: "PENDING" | "SENT" | "DELIVERED" | "OPENED" | "CLICKED" | "BOUNCED" | "COMPLAINED" | "FAILED";
- from?: string;
+ query: {
+ /** @description A sending domain in this project, e.g. `example.com`. */
+ domain: string;
+ /** @description Optionally, one RECIPIENT address to check as well. Adds its suppression state — the single most common reason a specific person stops receiving mail while everyone else still does. */
+ address?: string;
+ /** @description How far back the delivery counters look. 1–30 days; defaults to 7. */
+ window_days?: number;
};
header?: never;
path?: never;
@@ -4186,688 +11362,605 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Email list */
+ /** @description The diagnosis, with findings */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailListResponse"];
+ "application/json": components["schemas"]["DeliverabilityDiagnosisV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- sendEmail: {
+ v1ListDmarcReports: {
parameters: {
- query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description How far back to read, by the report's window start. 1-180 days; defaults to 30. */
+ days?: number;
+ /** @description Restrict to reports about one of your domains. */
+ domain?: string;
};
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendEmail"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Email accepted / sent */
+ /** @description Cursor-paginated DMARC aggregate reports */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SendEmailResponse"];
- };
- };
- /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DmarcReportV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- sendEmailBatch: {
+ v1ListRecipientDomainStats: {
parameters: {
- query?: never;
- header?: {
- "Idempotency-Key"?: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description How far back to read. 1-30 days; defaults to 30, which is the window the job maintains. */
+ days?: number;
+ /** @description Restrict to one recipient domain. */
+ domain?: string;
};
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["BatchSendBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description All entries sent */
+ /** @description Cursor-paginated recipient-domain rollup */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["BatchSendResponse"];
- };
- };
- /** @description Partial success — at least one entry failed */
- 207: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["BatchSendResponse"];
- };
- };
- /** @description Validation error, or `TOO_MANY_UNIQUE_TEMPLATES` — a new account submitted more distinct message bodies in one request than content review allows. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["RecipientDomainStatsV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions, project disabled, or `CONTENT_REJECTED`: the message was flagged by automated content review and was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONFLICT` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `IDEMPOTENCY_KEY_REUSED` — this `Idempotency-Key` was already used for a request with a different body. Reuse a key only to retry the identical request; otherwise send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description `CONTENT_REVIEW_UNAVAILABLE` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getEmail: {
+ v1ListDomains: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Email */
+ /** @description Sending domain list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailGetResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DomainV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- cancelScheduledEmail: {
+ v1CreateDomain: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["DomainV1Create"];
+ };
+ };
responses: {
- /** @description Email cancelled */
- 200: {
+ /** @description The registered sending domain, awaiting DNS */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailGetResponse"];
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — this domain is already registered to a project you can send from. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Email already past PENDING */
- 409: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `internal_error` — AWS SES rejected the identity setup (unusable credentials, a missing IAM permission, or a refusal on Amazon's side). No domain row is written, and the request is not at fault: retrying it unchanged will not help. */
+ 502: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- subscribeToList: {
+ v1GetDomain: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description List id. */
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ListSubscribe"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Contact subscribed, or an existing membership returned unchanged */
+ /** @description The sending domain */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ListSubscribeResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description The contact previously unsubscribed from this list and `allowResubscribe` was not set. `error.code` is `RESUBSCRIBE_CONFIRMATION_REQUIRED` and `error.details.previousStatus` is `UNSUBSCRIBED`. Retry with `allowResubscribe: true` once the contact has consented. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- unsubscribeFromList: {
+ v1DeleteDomain: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description List id. */
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["ListUnsubscribe"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Contact unsubscribed */
+ /** @description Sending domain removed */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ListUnsubscribeResponse"];
+ "application/json": components["schemas"]["DomainV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — the domain is still in use by a template, workflow step or active campaign. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listMailboxes: {
+ v1VerifyDomain: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Mailbox list */
+ /** @description The sending domain, as SES and DNS now report it */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Mailbox"][];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["DomainV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no sending domain with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createMailbox: {
+ v1ValidateEmails: {
parameters: {
query?: never;
header?: never;
@@ -4876,1126 +11969,1066 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateMailboxBody"];
+ "application/json": components["schemas"]["EmailValidationBatchRequestV1"];
};
};
responses: {
- /** @description Mailbox provisioned */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- data: components["schemas"]["Mailbox"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description One verdict per address, in the order they were given */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EmailValidationBatchV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
- };
- };
- /** @description Resource not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description The address already exists, the domain is not verified, or the project is at its 10-mailbox limit. */
- 409: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
- 429: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
- 500: {
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
+ 429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Provisioning failed in the mail server. The mailbox row is left `FAILED` and can be retried. */
- 502: {
+ /** @description `internal_error`. */
+ 500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getMailbox: {
+ v1SendEmail: {
parameters: {
query?: never;
- header?: never;
- path: {
- id: string;
+ header?: {
+ /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
+ "Idempotency-Key"?: string;
};
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendEmailV1"];
+ };
+ };
responses: {
- /** @description Mailbox with connection settings */
- 200: {
+ /** @description Email queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["MailboxDetail"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EmailV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_disabled`, `domain_not_allowed` — the `from` address does not resolve to a verified domain on this project — or `content_rejected`: automated content review flagged the message and it was not sent. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — `template` names a template that does not belong to this project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — the body did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. Send a new key. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `content_review_unavailable` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteMailbox: {
+ v1SendTestEmail: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SendTestEmailV1"];
+ };
+ };
responses: {
- /** @description Mailbox deleted */
- 200: {
+ /** @description Test email queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /** @enum {boolean} */
- deleted: true;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EmailTestV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_disabled`, or `forbidden` — the recipient is not the project owner's own verified account email (including the case where that address is not verified yet, so there is no default recipient), or `content_rejected` from automated content review. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `conflict` — this project has no sandbox sender. The handle is derived from the project owner's email and this project has no owner; no retry fixes it. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — the body did not match the schema, most often because it named a `from`. A test send always comes from the sandbox address. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — the project's daily sandbox send cap is spent. It resets at 00:00 UTC. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `content_review_unavailable` — content review could not run for this new account. Safe to retry. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listAppPasswords: {
+ v1ListEvents: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Return only events with this exact name. */
+ event_name?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description App password list */
+ /** @description Event list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["AppPassword"][];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createAppPassword: {
+ v1TrackEvent: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateAppPassword"];
+ "application/json": components["schemas"]["EventTrackV1"];
};
};
responses: {
- /** @description App password created; the secret is behind the one-time link */
+ /** @description Event recorded */
201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["AppPasswordReveal"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["EventV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no contact with this id in the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- revokeAppPassword: {
+ v1ListEventNames: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- passwordId: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description App password revoked */
+ /** @description Event names */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- /** @enum {boolean} */
- revoked: true;
- };
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventNamesV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listApiKeys: {
+ v1GetEventStats: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Project id. */
- id: string;
+ query?: {
+ /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
+ from?: string | null;
+ /** @description End of the window (ISO 8601). Defaults to now. */
+ to?: string | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description API key list */
+ /** @description Event counts */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ApiKeyListResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["EventStatsV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createApiKey: {
+ v1ListLists: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Project id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateApiKeyBody"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description API key created; the secret is behind the reveal link. */
- 201: {
+ /** @description Subscriber lists */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- /** @description An API key's metadata. Never carries the token or its hash — `lastFour` is the only surviving fragment of the secret once the key has been created. */
- data: components["schemas"]["ApiKey"] & {
- /**
- * Format: date-time
- * @description When the reveal link stops working. Create or rotate again to get a new one.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
- */
- revealUrl: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ListV1List"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- revokeApiKey: {
+ v1CreateList: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Project id. */
- id: string;
- /** @description API key id. */
- keyId: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
- responses: {
- /** @description API key revoked */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["SuccessEmpty"];
- };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ListV1Create"];
};
- /** @description Validation error */
- 400: {
+ };
+ responses: {
+ /** @description The created list */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- rotateApiKey: {
+ v1GetList: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Project id. */
+ /** @description Resource id. */
id: string;
- /** @description API key id. */
- keyId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description API key rotated; the new secret is behind the reveal link. */
+ /** @description The list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: {
- lastFour: string;
- /**
- * Format: date-time
- * @description When the reveal link stops working. Create or rotate again to get a new one.
- */
- revealExpiresAt: string;
- /**
- * Format: uri
- * @description A one-time, session-authenticated URL where the person who owns this project can see the secret. The secret itself is never returned to an API or agent caller: opening this link requires a signed-in dashboard session, so the credential that created the key cannot redeem it. Single use — the first successful open consumes it.
- */
- revealUrl: string;
- };
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listSuppressions: {
+ v1DeleteList: {
parameters: {
- query?: {
- limit?: number;
- cursor?: string;
- reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression list */
+ /** @description List deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuppressionListResponse"];
+ "application/json": components["schemas"]["ListV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- addSuppression: {
+ v1UpdateList: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["AddSuppression"];
+ "application/json": components["schemas"]["ListV1Update"];
};
};
responses: {
- /** @description Suppression added */
- 201: {
+ /** @description The updated list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Suppression"];
+ "application/json": components["schemas"]["ListV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- checkSuppression: {
+ v1StartListValidationRun: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description URL-encoded email address */
- email: string;
+ /** @description Resource id. */
+ id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression check result */
- 200: {
+ /** @description The run, accepted and queued */
+ 202: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SuppressionCheckResponse"];
+ "application/json": components["schemas"]["EmailValidationRunV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no list with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- removeSuppression: {
+ v1GetProject: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description URL-encoded email address */
- email: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Suppression removed */
- 204: {
+ /** @description The authenticated project */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": components["schemas"]["ProjectV1"];
+ };
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — the project was deleted between authentication and this read. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- listTemplates: {
+ v1ListSegments: {
parameters: {
query?: {
limit?: number;
- cursor?: string;
- search?: string;
- type?: "MARKETING" | "TRANSACTIONAL" | "HEADLESS";
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
header?: never;
path?: never;
@@ -6003,72 +13036,63 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Template list */
+ /** @description Segment list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["TemplateListResponse"];
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SegmentV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createTemplate: {
+ v1CreateSegment: {
parameters: {
query?: never;
header?: never;
@@ -6077,512 +13101,491 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CreateTemplate"];
+ "application/json": components["schemas"]["SegmentV1Create"];
};
};
responses: {
- /** @description Template created */
+ /** @description Segment created */
201: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
+ content: {
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Validation error */
+ /** @description `validation_error` — a `DYNAMIC` segment was submitted without a `condition`. */
400: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- getTemplate: {
+ v1GetSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Template */
+ /** @description The segment */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- deleteTemplate: {
+ v1DeleteSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Template deleted */
+ /** @description Segment deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["IdResponse"];
+ "application/json": components["schemas"]["SegmentV1Deleted"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
- 404: {
+ /** @description `conflict` — the segment is still used by one or more active campaigns. */
+ 409: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Template still in use */
- 409: {
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- updateTemplate: {
+ v1UpdateSegment: {
parameters: {
query?: never;
header?: never;
path: {
+ /** @description Resource id. */
id: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["UpdateTemplate"];
+ "application/json": components["schemas"]["SegmentV1Update"];
};
};
responses: {
- /** @description Updated template */
+ /** @description The updated segment */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": {
- data: components["schemas"]["Template"];
- /** @enum {boolean} */
- success: true;
- };
- };
- };
- /** @description Validation error */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SegmentV1"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Resource not found */
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- trackEvent: {
+ v1ListSegmentContacts: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["TrackEvent"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Event tracked */
+ /** @description Segment member list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["TrackEventResponse"];
+ "application/json": components["schemas"]["SegmentContactV1List"];
};
};
- /** @description Validation error */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Unauthorized — missing or invalid auth */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
- 403: {
+ /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- createProject: {
+ v1ListSuppressions: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Filter to one reason. Omit for every suppressed address. */
+ reason?: "HARD_BOUNCE" | "COMPLAINT" | "MANUAL" | "UNSUBSCRIBE";
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": {
- name: string;
- /**
- * @description AWS SES region for the project. Once a domain is added the region is locked and cannot be changed.
- * @enum {string}
- */
- sesRegion?: "us-east-1" | "us-west-2" | "eu-west-1";
- };
- };
- };
+ requestBody?: never;
responses: {
- /** @description Project created */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["ProjectRecord"];
- };
- };
- /** @description Validation error */
- 400: {
+ /** @description Suppression list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/json": components["schemas"]["SuppressionV1List"];
};
};
- /** @description Unauthorized — missing or invalid auth */
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
401: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Forbidden — insufficient permissions or project disabled */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Validation failed — request body or query parameters did not match the schema */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Rate limit or billing limit exceeded */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description Internal server error */
+ /** @description `internal_error`. */
500: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Error"];
+ "application/problem+json": components["schemas"]["Problem"];
};
};
};
};
- v1GetCampaignAnalytics: {
+ v1CreateSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["SuppressionV1Create"];
+ };
+ };
responses: {
- /** @description Campaign statistics */
- 200: {
+ /** @description The suppressed address */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsCampaignStatsV1"];
+ "application/json": components["schemas"]["SuppressionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6632,27 +13635,25 @@ interface operations {
};
};
};
- v1GetAnalyticsTimeseries: {
+ v1GetSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The suppressed address, URL-encoded. */
+ email: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Daily time series */
+ /** @description The suppression record */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsTimeseriesV1"];
+ "application/json": components["schemas"]["SuppressionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6673,6 +13674,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — this address is not suppressed for the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -6702,28 +13712,25 @@ interface operations {
};
};
};
- v1ListTopCampaigns: {
+ v1DeleteSuppression: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- limit?: number;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description The suppressed address, URL-encoded. */
+ email: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Ranked campaigns */
+ /** @description Address removed from the suppression list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["AnalyticsTopCampaignsV1"];
+ "application/json": components["schemas"]["SuppressionV1Deleted"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6773,12 +13780,15 @@ interface operations {
};
};
};
- v1ListCampaigns: {
+ v1ListTemplates: {
parameters: {
query?: {
limit?: number;
/** @description Opaque cursor from a previous response's `next_cursor`. */
after?: string;
+ /** @description Case-insensitive substring match on the name. */
+ search?: string;
+ email_category?: "TRANSACTIONAL" | "MARKETING" | "SELF_MANAGED_UNSUBSCRIBE";
};
header?: never;
path?: never;
@@ -6786,13 +13796,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Campaign list */
+ /** @description Template list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1List"];
+ "application/json": components["schemas"]["TemplateV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6842,29 +13852,26 @@ interface operations {
};
};
};
- v1CreateCampaign: {
+ v1CreateTemplate: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Create"];
+ "application/json": components["schemas"]["TemplateV1Create"];
};
};
responses: {
- /** @description Campaign created */
+ /** @description The created template */
201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6885,25 +13892,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — `segment_id` names a segment that does not belong to this project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -6932,7 +13921,7 @@ interface operations {
};
};
};
- v1GetCampaign: {
+ v1GetTemplate: {
parameters: {
query?: never;
header?: never;
@@ -6944,13 +13933,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description The campaign */
+ /** @description The template */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -6971,7 +13960,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7009,7 +13998,7 @@ interface operations {
};
};
};
- v1DeleteCampaign: {
+ v1DeleteTemplate: {
parameters: {
query?: never;
header?: never;
@@ -7021,17 +14010,17 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Campaign deleted */
+ /** @description Template deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1Deleted"];
+ "application/json": components["schemas"]["TemplateV1Deleted"];
};
};
- /** @description `validation_error` — only `DRAFT` campaigns can be deleted. */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -7039,8 +14028,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -7048,8 +14037,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -7057,8 +14046,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
+ /** @description `conflict` — the template is still referenced by a workflow step or an active campaign. */
+ 409: {
headers: {
[name: string]: unknown;
};
@@ -7095,7 +14084,7 @@ interface operations {
};
};
};
- v1UpdateCampaign: {
+ v1UpdateTemplate: {
parameters: {
query?: never;
header?: never;
@@ -7107,26 +14096,17 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Update"];
+ "application/json": components["schemas"]["TemplateV1Update"];
};
};
responses: {
- /** @description The updated campaign */
+ /** @description The updated template */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — the campaign is not in an editable status, or the segment change is not allowed. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TemplateV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7147,7 +14127,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no template with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7185,34 +14165,27 @@ interface operations {
};
};
};
- v1CancelCampaign: {
+ v1ListTopics: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Resource id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ include_archived?: boolean | null;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The cancelled campaign */
+ /** @description One page of topics */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — only `SCHEDULED`, `SENDING`, or `PAUSED` campaigns can be cancelled. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicListV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7233,15 +14206,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7271,34 +14235,26 @@ interface operations {
};
};
};
- v1PauseCampaign: {
+ v1CreateTopic: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Resource id. */
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
- responses: {
- /** @description The paused campaign */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TopicCreateV1"];
};
- /** @description `validation_error` — only a `SENDING` campaign can be paused. */
- 400: {
+ };
+ responses: {
+ /** @description The created topic */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7319,15 +14275,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7357,7 +14304,7 @@ interface operations {
};
};
};
- v1ResumeCampaign: {
+ v1GetTopic: {
parameters: {
query?: never;
header?: never;
@@ -7369,22 +14316,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description The resumed campaign */
+ /** @description The topic */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — only a `PAUSED` campaign can be resumed. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7405,7 +14343,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7443,41 +14381,29 @@ interface operations {
};
};
};
- v1SendCampaign: {
+ v1UpdateTopic: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path: {
/** @description Resource id. */
id: string;
};
cookie?: never;
};
- requestBody?: {
+ requestBody: {
content: {
- "application/json": components["schemas"]["CampaignV1Send"];
+ "application/json": components["schemas"]["TopicUpdateV1"];
};
};
responses: {
- /** @description The campaign, now `SENDING` or `SCHEDULED` */
+ /** @description The updated topic */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1"];
- };
- };
- /** @description `validation_error` — the campaign has already been sent or is sending, has no recipients, or `scheduled_for` is not in the future. */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["TopicV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7498,7 +14424,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7507,16 +14433,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the request did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. A key names ONE request, so it is never silently served another one's result: send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7545,7 +14462,7 @@ interface operations {
};
};
};
- v1GetCampaignStats: {
+ v1SetTopicSubscription: {
parameters: {
query?: never;
header?: never;
@@ -7555,15 +14472,19 @@ interface operations {
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TopicSubscribeV1"];
+ };
+ };
responses: {
- /** @description Campaign statistics */
+ /** @description The resulting subscription */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["CampaignV1Stats"];
+ "application/json": components["schemas"]["TopicSubscriptionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7584,7 +14505,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no campaign with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no topic with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -7622,29 +14543,22 @@ interface operations {
};
};
};
- v1SendEmail: {
+ v1GetUsage: {
parameters: {
query?: never;
- header?: {
- /** @description Replay-safety key (24h TTL). Reuse it only to retry the identical request. */
- "Idempotency-Key"?: string;
- };
+ header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendEmailV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Email queued */
- 202: {
+ /** @description Current usage */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailV1"];
+ "application/json": components["schemas"]["UsageV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7656,7 +14570,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_disabled`, `domain_not_allowed` — the `from` address does not resolve to a verified domain on this project — or `content_rejected`: automated content review flagged the message and it was not sent. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
@@ -7665,25 +14579,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — `template` names a template that does not belong to this project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — a request with this `Idempotency-Key` is still in flight. Retry shortly. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `validation_error` — the body did not match the schema; or `idempotency_key_reused` — this `Idempotency-Key` was already spent on a request with a different body. Send a new key. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7710,37 +14606,27 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `content_review_unavailable` — content review could not run for this new account, so the message was not accepted. Safe to retry after a short delay. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
};
};
- v1SendTestEmail: {
+ v1GetValidationRun: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["SendTestEmailV1"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Test email queued */
- 202: {
+ /** @description The run */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EmailTestV1"];
+ "application/json": components["schemas"]["EmailValidationRunV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7752,7 +14638,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_disabled`, or `forbidden` — the recipient is not the project owner's own verified account email (including the case where that address is not verified yet, so there is no default recipient), or `content_rejected` from automated content review. */
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
403: {
headers: {
[name: string]: unknown;
@@ -7761,8 +14647,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — this project has no sandbox sender. The handle is derived from the project owner's email and this project has no owner; no retry fixes it. */
- 409: {
+ /** @description `resource_not_found` — no validation run with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -7770,7 +14656,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `validation_error` — the body did not match the schema, most often because it named a `from`. A test send always comes from the sandbox address. */
+ /** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
[name: string]: unknown;
@@ -7779,7 +14665,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `rate_limited` — the project's daily sandbox send cap is spent. It resets at 00:00 UTC. */
+ /** @description `rate_limited` — see `Retry-After` and the `RateLimit` headers. */
429: {
headers: {
[name: string]: unknown;
@@ -7797,39 +14683,33 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `content_review_unavailable` — content review could not run for this new account. Safe to retry. */
- 503: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
};
};
- v1ListEvents: {
+ v1ListValidationRunResults: {
parameters: {
query?: {
limit?: number;
/** @description Opaque cursor from a previous response's `next_cursor`. */
after?: string;
- /** @description Return only events with this exact name. */
- event_name?: string;
+ /** @description Return only results with this verdict — `undeliverable` is the usual filter. */
+ verdict?: components["schemas"]["EmailValidationVerdictV1"] & unknown;
};
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Event list */
+ /** @description One page of results */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventV1List"];
+ "application/json": components["schemas"]["EmailValidationResultListV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7850,6 +14730,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no validation run with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7879,26 +14768,26 @@ interface operations {
};
};
};
- v1TrackEvent: {
+ v1ListWebhooks: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["EventTrackV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Event recorded */
- 201: {
+ /** @description Webhook list */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventV1"];
+ "application/json": components["schemas"]["WebhookV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -7919,15 +14808,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no contact with this id in the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -7957,22 +14837,26 @@ interface operations {
};
};
};
- v1ListEventNames: {
+ v1CreateWebhook: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookV1Create"];
+ };
+ };
responses: {
- /** @description Event names */
- 200: {
+ /** @description The created webhook and its one-time signing secret */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventNamesV1"];
+ "application/json": components["schemas"]["WebhookV1Created"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8022,27 +14906,25 @@ interface operations {
};
};
};
- v1GetEventStats: {
+ v1GetWebhook: {
parameters: {
- query?: {
- /** @description Start of the window (ISO 8601). Defaults to 30 days ago; clamped to at most 90 days back. */
- from?: string | null;
- /** @description End of the window (ISO 8601). Defaults to now. */
- to?: string | null;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Event counts */
+ /** @description The webhook */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["EventStatsV1"];
+ "application/json": components["schemas"]["WebhookV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8063,6 +14945,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8092,22 +14983,25 @@ interface operations {
};
};
};
- v1GetProject: {
+ v1DeleteWebhook: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The authenticated project */
+ /** @description Webhook deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["ProjectV1"];
+ "application/json": components["schemas"]["WebhookV1Deleted"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8128,7 +15022,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — the project was deleted between authentication and this read. */
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8166,26 +15060,29 @@ interface operations {
};
};
};
- v1ListSegments: {
+ v1UpdateWebhook: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Resource id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookV1Update"];
+ };
+ };
responses: {
- /** @description Segment list */
+ /** @description The updated webhook */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1List"];
+ "application/json": components["schemas"]["WebhookV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8206,6 +15103,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8235,30 +15141,29 @@ interface operations {
};
};
};
- v1CreateSegment: {
+ v1RotateWebhookSecret: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- "application/json": components["schemas"]["SegmentV1Create"];
+ path: {
+ /** @description Resource id. */
+ id: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Segment created */
- 201: {
+ /** @description The new signing secret and the moment the previous one stops verifying */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
+ "application/json": components["schemas"]["WebhookV1SecretRotated"];
};
};
- /** @description `validation_error` — a `DYNAMIC` segment was submitted without a `condition`. */
- 400: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -8266,8 +15171,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -8275,8 +15180,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `resource_not_found` — no webhook with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -8313,38 +15218,30 @@ interface operations {
};
};
};
- v1GetSegment: {
+ v1ListWorkflows: {
parameters: {
- query?: never;
- header?: never;
- path: {
- /** @description Resource id. */
- id: string;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description The segment */
+ /** @description Workflow list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
- };
- };
- /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
+ "application/json": components["schemas"]["WorkflowV1List"];
};
};
- /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
+ /** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -8352,8 +15249,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
- 404: {
+ /** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -8390,25 +15287,26 @@ interface operations {
};
};
};
- v1DeleteSegment: {
+ v1CreateWorkflow: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Resource id. */
- id: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowCreateV1"];
+ };
+ };
responses: {
- /** @description Segment deleted */
- 200: {
+ /** @description Workflow created */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1Deleted"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8429,24 +15327,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `conflict` — the segment is still used by one or more active campaigns. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8476,29 +15356,25 @@ interface operations {
};
};
};
- v1UpdateSegment: {
+ v1CancelWorkflowExecution: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Resource id. */
- id: string;
+ /** @description Workflow execution id. */
+ execution_id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["SegmentV1Update"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description The updated segment */
+ /** @description Cancelled execution */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8519,7 +15395,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no execution with this id in the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8557,29 +15433,25 @@ interface operations {
};
};
};
- v1ListSegmentContacts: {
+ v1GetWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
path: {
- /** @description Resource id. */
+ /** @description Workflow id. */
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Segment member list */
+ /** @description Workflow */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["SegmentContactV1List"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8600,7 +15472,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no segment with this id belongs to the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8638,22 +15510,25 @@ interface operations {
};
};
};
- v1GetUsage: {
+ v1DeleteWorkflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Current usage */
+ /** @description Workflow deleted */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["UsageV1"];
+ "application/json": components["schemas"]["WorkflowDeletedV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8674,6 +15549,24 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — the workflow still has running executions. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8703,26 +15596,29 @@ interface operations {
};
};
};
- v1ListWorkflows: {
+ v1UpdateWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- };
+ query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowUpdateV1"];
+ };
+ };
responses: {
- /** @description Workflow list */
+ /** @description Updated workflow */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1List"];
+ "application/json": components["schemas"]["WorkflowV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8743,6 +15639,24 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
+ /** @description `conflict` — the trigger cannot be changed while executions are running. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8772,20 +15686,23 @@ interface operations {
};
};
};
- v1CreateWorkflow: {
+ v1CloneWorkflow: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ /** @description Workflow id. */
+ id: string;
+ };
cookie?: never;
};
- requestBody: {
+ requestBody?: {
content: {
- "application/json": components["schemas"]["WorkflowCreateV1"];
+ "application/json": components["schemas"]["WorkflowCloneV1"];
};
};
responses: {
- /** @description Workflow created */
+ /** @description The cloned workflow */
201: {
headers: {
[name: string]: unknown;
@@ -8812,6 +15729,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8841,25 +15767,31 @@ interface operations {
};
};
};
- v1CancelWorkflowExecution: {
+ v1ListWorkflowExecutions: {
parameters: {
- query?: never;
+ query?: {
+ limit?: number;
+ /** @description Opaque cursor from a previous response's `next_cursor`. */
+ after?: string;
+ /** @description Return only executions in this state. */
+ status?: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
+ };
header?: never;
path: {
- /** @description Workflow execution id. */
- execution_id: string;
+ /** @description Workflow id. */
+ id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Cancelled execution */
+ /** @description Execution list */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1List"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8880,7 +15812,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no execution with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -8918,7 +15850,7 @@ interface operations {
};
};
};
- v1GetWorkflow: {
+ v1StartWorkflowExecution: {
parameters: {
query?: never;
header?: never;
@@ -8928,15 +15860,19 @@ interface operations {
};
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WorkflowExecutionStartV1"];
+ };
+ };
responses: {
- /** @description Workflow */
- 200: {
+ /** @description Execution started */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1"];
+ "application/json": components["schemas"]["WorkflowExecutionV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -8957,7 +15893,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no such workflow, or no such contact in this project. */
404: {
headers: {
[name: string]: unknown;
@@ -8966,6 +15902,15 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
+ /** @description `conflict` — the contact already has an execution and re-entry is not allowed. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["Problem"];
+ };
+ };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -8995,7 +15940,7 @@ interface operations {
};
};
};
- v1DeleteWorkflow: {
+ v1GetWorkflowGraph: {
parameters: {
query?: never;
header?: never;
@@ -9007,13 +15952,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Workflow deleted */
+ /** @description The workflow's graph */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowDeletedV1"];
+ "application/json": components["schemas"]["WorkflowGraphV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9034,7 +15979,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9043,15 +15988,6 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the workflow still has running executions. */
- 409: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
/** @description `validation_error` — query, path, or body parameters did not match the schema. */
422: {
headers: {
@@ -9081,7 +16017,7 @@ interface operations {
};
};
};
- v1UpdateWorkflow: {
+ v1ReplaceWorkflowGraph: {
parameters: {
query?: never;
header?: never;
@@ -9093,17 +16029,17 @@ interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["WorkflowUpdateV1"];
+ "application/json": components["schemas"]["WorkflowGraphReplaceV1"];
};
};
responses: {
- /** @description Updated workflow */
+ /** @description The graph as it now stands */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowV1"];
+ "application/json": components["schemas"]["WorkflowGraphV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9124,7 +16060,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9133,7 +16069,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the trigger cannot be changed while executions are running. */
+ /** @description `conflict` — the workflow has running executions, or an id in the document already belongs to a different workflow. */
409: {
headers: {
[name: string]: unknown;
@@ -9171,15 +16107,9 @@ interface operations {
};
};
};
- v1ListWorkflowExecutions: {
+ v1PauseWorkflow: {
parameters: {
- query?: {
- limit?: number;
- /** @description Opaque cursor from a previous response's `next_cursor`. */
- after?: string;
- /** @description Return only executions in this state. */
- status?: "RUNNING" | "WAITING" | "COMPLETED" | "EXITED" | "FAILED" | "CANCELLED";
- };
+ query?: never;
header?: never;
path: {
/** @description Workflow id. */
@@ -9189,13 +16119,13 @@ interface operations {
};
requestBody?: never;
responses: {
- /** @description Execution list */
+ /** @description The workflow, and the number of runs this call cancelled */
200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1List"];
+ "application/json": components["schemas"]["WorkflowStateChangeV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9216,7 +16146,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -9254,7 +16184,7 @@ interface operations {
};
};
};
- v1StartWorkflowExecution: {
+ v1ResumeWorkflow: {
parameters: {
query?: never;
header?: never;
@@ -9264,19 +16194,15 @@ interface operations {
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["WorkflowExecutionStartV1"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Execution started */
- 201: {
+ /** @description The workflow, with `cancelled_executions` always 0 */
+ 200: {
headers: {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["WorkflowExecutionV1"];
+ "application/json": components["schemas"]["WorkflowStateChangeV1"];
};
};
/** @description `invalid_api_key` or `invalid_session` — missing or invalid credentials. */
@@ -9289,16 +16215,7 @@ interface operations {
};
};
/** @description `scope_missing`, `project_access_denied`, or `project_disabled`. */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/problem+json": components["schemas"]["Problem"];
- };
- };
- /** @description `resource_not_found` — no such workflow, or no such contact in this project. */
- 404: {
+ 403: {
headers: {
[name: string]: unknown;
};
@@ -9306,8 +16223,8 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `conflict` — the contact already has an execution and re-entry is not allowed. */
- 409: {
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -9386,7 +16303,7 @@ interface operations {
"application/problem+json": components["schemas"]["Problem"];
};
};
- /** @description `resource_not_found` — no workflow with this id in the authenticated project. */
+ /** @description `resource_not_found` — no workflow with this id belongs to the authenticated project. */
404: {
headers: {
[name: string]: unknown;
@@ -10009,7 +16926,23 @@ type BatchSendResponse = components["schemas"]["BatchSendResponse"];
type BatchEntryResult = components["schemas"]["BatchEntryResult"];
type EmailRecord = components["schemas"]["Email"];
type EmailListResponse = components["schemas"]["EmailListResponse"];
-type EmailGetResponse = components["schemas"]["EmailGetResponse"];
+/**
+ * One transition in a message's delivery history — the append-only record
+ * behind `status`. `status` says where the message is now; these say how it got
+ * there.
+ */
+type EmailEvent = components["schemas"]["EmailEvent"];
+/** An email together with its delivery history, oldest first. */
+type EmailWithEvents = components["schemas"]["EmailWithEvents"];
+/**
+ * A single email with no history — what `emails.cancelSchedule` resolves.
+ *
+ * Was `EmailGetResponse` in 1.0, which named the operation rather than the
+ * shape and was then reused by an operation that is not a GET.
+ */
+type EmailResponse = components["schemas"]["EmailResponse"];
+/** `emails.get` — one email plus its delivery events. */
+type EmailDetailResponse = components["schemas"]["EmailDetailResponse"];
type ListEmailsQuery = NonNullable;
type ContactRecord = components["schemas"]["Contact"];
type ContactListResponse = components["schemas"]["ContactListResponse"];
@@ -10021,6 +16954,8 @@ type ListContactsQuery = NonNullable;
type SuppressionRecord = components["schemas"]["Suppression"];
type SuppressionListResponse = components["schemas"]["SuppressionListResponse"];
type SuppressionCheckResponse = components["schemas"]["SuppressionCheckResponse"];
@@ -10069,8 +17020,8 @@ type CampaignV1 = components["schemas"]["CampaignV1"];
type CampaignListV1 = components["schemas"]["CampaignV1List"];
type CampaignDeletedV1 = components["schemas"]["CampaignV1Deleted"];
type CampaignStatsV1 = components["schemas"]["CampaignV1Stats"];
-/** `type` defaults to `MARKETING` server-side, so it is optional here. */
-type CreateCampaignV1Request = PartialKeys;
+/** `email_category` defaults to `MARKETING` server-side, so it is optional here. */
+type CreateCampaignV1Request = PartialKeys;
type UpdateCampaignV1Request = components["schemas"]["CampaignV1Update"];
type SendCampaignV1Request = components["schemas"]["CampaignV1Send"];
type ListCampaignsV1Query = NonNullable;
@@ -10116,6 +17067,92 @@ type EmailTestV1 = components["schemas"]["EmailTestV1"];
type AnalyticsTimeseriesV1Query = NonNullable;
type AnalyticsCampaignsV1Query = NonNullable;
type ListTopCampaignsV1Query = NonNullable;
+type ContactV1 = components["schemas"]["ContactV1"];
+type ContactListV1 = components["schemas"]["ContactV1List"];
+type ContactDeletedV1 = components["schemas"]["ContactV1Deleted"];
+/** `subscribed` defaults to `true` server-side, so it is optional here. */
+type CreateContactV1Request = PartialKeys;
+type UpdateContactV1Request = components["schemas"]["ContactV1Update"];
+/** Everything one contact has said they want, topic by topic. */
+type ContactTopicPreferencesV1 = components["schemas"]["ContactTopicPreferencesV1"];
+type ListContactsV1Query = NonNullable;
+type ListV1 = components["schemas"]["ListV1"];
+type ListListV1 = components["schemas"]["ListV1List"];
+type ListDeletedV1 = components["schemas"]["ListV1Deleted"];
+/** `double_opt_in` defaults to `false` server-side, so it is optional here. */
+type CreateListV1Request = PartialKeys;
+type UpdateListV1Request = components["schemas"]["ListV1Update"];
+type ListListsV1Query = NonNullable;
+type TemplateV1 = components["schemas"]["TemplateV1"];
+type TemplateListV1 = components["schemas"]["TemplateV1List"];
+type TemplateDeletedV1 = components["schemas"]["TemplateV1Deleted"];
+/** `email_category` defaults to `MARKETING` server-side, so it is optional here. */
+type CreateTemplateV1Request = PartialKeys;
+type UpdateTemplateV1Request = components["schemas"]["TemplateV1Update"];
+type ListTemplatesV1Query = NonNullable;
+type DomainV1 = components["schemas"]["DomainV1"];
+type DomainListV1 = components["schemas"]["DomainV1List"];
+type DomainDeletedV1 = components["schemas"]["DomainV1Deleted"];
+type CreateDomainV1Request = components["schemas"]["DomainV1Create"];
+type ListDomainsV1Query = NonNullable;
+type WebhookV1 = components["schemas"]["WebhookV1"];
+type WebhookListV1 = components["schemas"]["WebhookV1List"];
+type WebhookDeletedV1 = components["schemas"]["WebhookV1Deleted"];
+/** The create response, and the only time the signing secret is readable. */
+type WebhookCreatedV1 = components["schemas"]["WebhookV1Created"];
+/** Rotation answers the new secret once, for the same reason. */
+type WebhookSecretRotatedV1 = components["schemas"]["WebhookV1SecretRotated"];
+type CreateWebhookV1Request = components["schemas"]["WebhookV1Create"];
+type UpdateWebhookV1Request = components["schemas"]["WebhookV1Update"];
+type ListWebhooksV1Query = NonNullable;
+type SuppressionV1 = components["schemas"]["SuppressionV1"];
+type SuppressionListV1 = components["schemas"]["SuppressionV1List"];
+type SuppressionDeletedV1 = components["schemas"]["SuppressionV1Deleted"];
+/** `reason` defaults to `MANUAL` server-side, so it is optional here. */
+type CreateSuppressionV1Request = PartialKeys;
+type ListSuppressionsV1Query = NonNullable;
+type TopicV1 = components["schemas"]["TopicV1"];
+type TopicListV1 = components["schemas"]["TopicListV1"];
+type CreateTopicV1Request = components["schemas"]["TopicCreateV1"];
+type UpdateTopicV1Request = components["schemas"]["TopicUpdateV1"];
+type SetTopicSubscriptionV1Request = components["schemas"]["TopicSubscribeV1"];
+type TopicSubscriptionV1 = components["schemas"]["TopicSubscriptionV1"];
+type TopicSubscriptionStatusV1 = components["schemas"]["TopicSubscriptionStatusV1"];
+type ListTopicsV1Query = NonNullable;
+type ValidateEmailsV1Request = components["schemas"]["EmailValidationBatchRequestV1"];
+type EmailValidationBatchV1 = components["schemas"]["EmailValidationBatchV1"];
+type EmailValidationV1 = components["schemas"]["EmailValidationV1"];
+type EmailValidationVerdictV1 = components["schemas"]["EmailValidationVerdictV1"];
+type EmailValidationRunV1 = components["schemas"]["EmailValidationRunV1"];
+type EmailValidationResultListV1 = components["schemas"]["EmailValidationResultListV1"];
+/**
+ * One address's verdict inside a run's results — a validation plus the
+ * `contact_id` it came from. The spec composes it inline rather than naming a
+ * component, so it is read off the page it appears in.
+ */
+type EmailValidationResultV1 = EmailValidationResultListV1["data"][number];
+type ListValidationResultsV1Query = NonNullable;
+type DeliverabilityDiagnosisV1 = components["schemas"]["DeliverabilityDiagnosisV1"];
+type DeliverabilityFindingV1 = components["schemas"]["DeliverabilityFindingV1"];
+type DeliverabilityFindingSeverityV1 = components["schemas"]["DeliverabilityFindingSeverityV1"];
+type DeliverabilityIdentityV1 = components["schemas"]["DeliverabilityIdentityV1"];
+type DeliverabilityRecentDeliveryV1 = components["schemas"]["DeliverabilityRecentDeliveryV1"];
+type DeliverabilitySuppressionV1 = components["schemas"]["DeliverabilitySuppressionV1"];
+type RecipientDomainStatsV1 = components["schemas"]["RecipientDomainStatsV1"];
+type RecipientDomainStatsListV1 = components["schemas"]["RecipientDomainStatsV1List"];
+type DmarcReportV1 = components["schemas"]["DmarcReportV1"];
+type DmarcReportListV1 = components["schemas"]["DmarcReportV1List"];
+type DiagnoseDeliverabilityV1Query = NonNullable;
+type ListRecipientDomainStatsV1Query = NonNullable;
+type ListDmarcReportsV1Query = NonNullable;
+type CampaignFailureV1 = components["schemas"]["CampaignV1Failure"];
+type CampaignFailureListV1 = components["schemas"]["CampaignV1FailureList"];
+type CampaignRetryFailedV1 = components["schemas"]["CampaignV1RetryFailed"];
+type ListCampaignFailuresV1Query = NonNullable;
+type WorkflowGraphV1 = components["schemas"]["WorkflowGraphV1"];
+type ReplaceWorkflowGraphV1Request = components["schemas"]["WorkflowGraphReplaceV1"];
+type CloneWorkflowV1Request = components["schemas"]["WorkflowCloneV1"];
+type WorkflowStateChangeV1 = components["schemas"]["WorkflowStateChangeV1"];
/**
* Sending analytics on the `/api/v1` surface.
@@ -10200,8 +17237,43 @@ declare class CampaignsResource {
resume(id: string): Promise;
/** Delivery and engagement counters plus derived rates for one campaign. */
stats(id: string): Promise;
+ /**
+ * The recipients this campaign did not reach, and why.
+ *
+ * {@link stats} says how many sends failed; only this says who. `reason`
+ * comes from a fixed vocabulary rather than the underlying error text, so it
+ * is stable enough to branch on — and it is `null` on rows recorded before
+ * reasons were captured.
+ *
+ * Cursor-paginated like every other v1 list, but uniquely it also carries
+ * `total`: {@link retryFailed} acts on that number, and `has_more` alone
+ * cannot tell you whether 3 or 30,000 sends failed.
+ */
+ listFailures(id: string, query?: ListCampaignFailuresV1Query): Promise;
+ /** Iterate every failed send across pages, yielding one recipient at a time. */
+ listFailuresAll(id: string, query?: ListCampaignFailuresV1Query): AsyncGenerator;
+ /**
+ * Re-drive only the recipients whose send failed. Nobody who already received
+ * the campaign is mailed a second time — each ledger row is claimed before it
+ * is touched, and a row whose email exists already is re-queued, not re-sent.
+ *
+ * The walk runs in the background, so this resolves as soon as it is queued,
+ * reporting `queued`: how many failed rows it was started for. Only a `SENT`
+ * campaign qualifies (`400 validation_error` otherwise), and a retry already
+ * running answers `409 conflict`. Takes no body.
+ */
+ retryFailed(id: string): Promise;
}
+/**
+ * Contacts, on both surfaces.
+ *
+ * The unsuffixed methods speak the legacy `/api/*` dialect — camelCase bodies
+ * inside a `{ success, data }` envelope the SDK unwraps. The `V1`-suffixed
+ * methods speak `/api/v1`: bare snake_case bodies, cursor pagination, and
+ * RFC 9457 problem documents on error. Both answer the same questions, so the
+ * suffix is there to keep a call site from confusing one for the other.
+ */
declare class ContactsResource {
private readonly client;
constructor(client: Sendly);
@@ -10221,8 +17293,138 @@ declare class ContactsResource {
update(id: string, body: UpdateContactRequest): Promise;
/** Delete a contact. The API answers 200 with `{ success, data: { id } }`; the SDK resolves void. */
delete(id: string): Promise;
+ /**
+ * List contacts on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after` with no total count, narrowed by
+ * `search` (case-insensitive substring on the address) and `subscribed`.
+ * Hold the filters steady for the whole walk — the cursor encodes them, and
+ * changing one mid-pagination returns `422 validation_error` asking you to
+ * restart. {@link listAllV1} drives the loop for you.
+ */
+ listV1(query?: ListContactsV1Query): Promise;
+ /** Iterate every v1 contact across pages, yielding one contact at a time. */
+ listAllV1(query?: ListContactsV1Query): AsyncGenerator;
+ /**
+ * Create a contact. Only `email` is required — `subscribed` defaults to true
+ * server-side, and `custom_fields` is arbitrary JSON that templates can read
+ * back as `{{ variables }}`.
+ */
+ createV1(body: CreateContactV1Request): Promise;
+ /**
+ * Retrieve a single contact by id. v1 has no lookup-by-address route — reach
+ * a contact you only know the email of through {@link listV1}'s `search`.
+ */
+ getV1(id: string): Promise;
+ /**
+ * Patch a contact. Only the fields you send are changed, with two caveats.
+ *
+ * `email` is not patchable at all: an address is the contact's identity here,
+ * and rewriting it in place would change who every earlier send was addressed
+ * to. Create the new address instead.
+ *
+ * `custom_fields` is **replaced, not merged** — the object you send becomes
+ * the whole of it, so read the contact and send back every key you mean to
+ * keep. Sending a partial object silently drops the rest.
+ */
+ updateV1(id: string, body: UpdateContactV1Request): Promise;
+ /**
+ * Delete a contact. Unlike the legacy {@link delete}, this resolves the
+ * `{ id, deleted }` acknowledgement rather than discarding it.
+ */
+ deleteV1(id: string): Promise;
+ /**
+ * Read everything this contact has said about what they want.
+ *
+ * The top-level `subscribed` is the global marketing opt-out and outranks
+ * every topic: false means nothing marketing reaches them whatever the topic
+ * rows say. Each topic's own `subscribed` is the effective answer the send
+ * path reaches today, with the topic's `default_opt_in` already folded in, so
+ * a contact who has never answered still reads correctly.
+ */
+ topicPreferences(id: string): Promise;
+}
+
+/**
+ * Deliverability on the `/api/v1` surface — why mail from your domains is, or
+ * is not, arriving.
+ *
+ * Responses are bare v1 bodies (no `{ success, data }` envelope) and errors are
+ * RFC 9457 problem documents.
+ */
+declare class DeliverabilityResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * Diagnose one of your SENDING domains: its DNS identity, the project's
+ * recent delivery outcomes, optionally one recipient's suppression state, and
+ * the `findings` drawn from them, worst first. Branch on a finding's `code`,
+ * never on its prose.
+ *
+ * `query.domain` is required — the endpoint answers about one domain. The
+ * optional `address` is a RECIPIENT to check alongside it, because being
+ * suppressed is the single most common reason one person stops receiving mail
+ * while everyone else still does. `window_days` (1–30, default 7) only moves
+ * the delivery counters.
+ *
+ * Nothing here is looked up live: the DNS statuses are the verification
+ * refresh job's cached results, and `identity.last_checked_at` says when they
+ * were filled. `recent_delivery` is project-wide rather than per-domain — its
+ * own `scope` field says so — because an email row records no sending domain.
+ */
+ diagnose(query: DiagnoseDeliverabilityV1Query): Promise;
+ /**
+ * Delivery outcomes broken out by RECIPIENT domain and UTC day, newest day
+ * first.
+ *
+ * These are the domains you send TO — `gmail.com`, `outlook.com` — not the
+ * domains you send FROM. That is the axis {@link diagnose} cannot report: its
+ * project-wide rates hide the case that matters most, one recipient domain
+ * refusing nearly everything while the rest of your mail is healthy.
+ *
+ * Cursor-paginated on `limit` + `after`. The counts come from an hourly
+ * rollup job over a rolling 30-day window, not from a query run on request;
+ * each row's `computed_at` says when it was last rebuilt. No rate is
+ * published, because a rate over three sends is not information.
+ */
+ listDomainStats(query?: ListRecipientDomainStatsV1Query): Promise;
+ /** Iterate every recipient-domain row across pages, one day-and-domain at a time. */
+ listDomainStatsAll(query?: ListRecipientDomainStatsV1Query): AsyncGenerator;
+ /**
+ * DMARC aggregate (RUA) reports that receiving providers have sent about your
+ * domains, newest reporting window first. Cursor-paginated on `limit` +
+ * `after`.
+ *
+ * An empty list is the correct answer, not a bug, until a policy domain is
+ * registered in this project and its DMARC record names an address we
+ * receive: only reports about a registered domain are stored, and receivers
+ * send them on their own schedule (typically once a day).
+ *
+ * `intake_configured` says which kind of empty you are looking at. When it is
+ * `false` this deployment has no DMARC report intake mailbox at all, so no
+ * report can ever arrive and an empty `data` means the feature is off — not
+ * that your domains are clean. The two are otherwise indistinguishable, so
+ * read the flag before reporting "no DMARC failures" to anyone.
+ *
+ * `pass_count` counts DMARC ALIGNMENT taken from `policy_evaluated`, not raw
+ * authentication results — a message can pass SPF for a domain that is not
+ * the one in its From header, which is exactly the case DMARC exists to
+ * catch.
+ */
+ listDmarcReports(query?: ListDmarcReportsV1Query): Promise;
+ /** Iterate every DMARC report across pages, one report at a time. */
+ listDmarcReportsAll(query?: ListDmarcReportsV1Query): AsyncGenerator;
}
+/**
+ * Sending domains, on both surfaces.
+ *
+ * The unsuffixed methods speak the legacy `/api/*` dialect — camelCase bodies
+ * inside a `{ success, data }` envelope the SDK unwraps. The `V1`-suffixed
+ * methods speak `/api/v1`: bare snake_case bodies, cursor pagination, and
+ * RFC 9457 problem documents on error. Both answer the same questions, so the
+ * suffix is there to keep a call site from confusing one for the other.
+ */
declare class DomainsResource {
private readonly client;
constructor(client: Sendly);
@@ -10233,14 +17435,24 @@ declare class DomainsResource {
* `eu-west-1`). On the very first domain for a project this also locks the
* project's region; subsequent calls must match.
*
- * The response includes DNS records to set.
+ * The response carries `dkimTokens` — the SES DKIM tokens to publish as
+ * CNAME records before the domain can verify — alongside `dkimStatus`,
+ * `spfStatus` and `dmarcStatus`, each the result of the last DNS check for
+ * that record type.
*/
create(body: AddDomainRequest): Promise;
/** List all domains for the project. */
list(): Promise;
/** Fetch a single domain. */
get(id: string): Promise;
- /** Trigger SES verification for a domain. */
+ /**
+ * Trigger SES verification for a domain.
+ *
+ * `status` is SES's own raw DKIM verification state (`Success`, `Pending`),
+ * while `dkimStatus`, `spfStatus` and `dmarcStatus` are this platform's own
+ * DNS check per record type. `tokens` carries the DKIM tokens SES has still
+ * to report and is absent once verification has resolved.
+ */
verify(id: string): Promise;
/** Read current SES verification status for a domain. */
getVerification(id: string): Promise;
@@ -10254,8 +17466,77 @@ declare class DomainsResource {
* back the link, not to model the flow behind it.
*/
startSetup(id: string): Promise;
+ /**
+ * Assign this sending identity to transactional or marketing traffic.
+ *
+ * Streams are enforced, not labelled: once assigned, a send of the other kind
+ * from this identity is refused with 403 — which is what keeps a campaign's
+ * complaint rate off the identity your password resets go out on. Pass
+ * `stream: null` to unassign, returning it to carrying both.
+ *
+ * `streamDefault` demotes whichever identity currently holds the default for
+ * that stream, and `defaultFromAddress` has to be an address on this
+ * identity's own host. Every field is optional; an omitted one is left alone.
+ *
+ * Legacy dialect: camelCase body, and the updated domain arrives inside the
+ * `{ success, data }` envelope this method unwraps for you.
+ */
+ assignStream(id: string, body: AssignDomainStreamRequest): Promise;
/** Delete a domain. */
delete(id: string): Promise;
+ /**
+ * List sending domains, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. {@link listAllV1}
+ * drives the loop for you.
+ *
+ * `verified` is SES's verdict on the identity and is what decides whether mail
+ * can leave from this domain; `dkim_verified` is a separate fact — what the
+ * DNS health refresh last read for the DKIM records — so the two disagree
+ * while a re-check is in flight and neither is a spelling of the other.
+ */
+ listV1(query?: ListDomainsV1Query): Promise;
+ /** Iterate every sending domain across pages, yielding one domain at a time. */
+ listAllV1(query?: ListDomainsV1Query): AsyncGenerator;
+ /**
+ * Register a sending domain and start SES DKIM verification.
+ *
+ * The identity comes back with `verified: false` — nothing is verified until
+ * the DKIM records are published in the domain's own DNS and SES resolves
+ * them, so poll {@link verifyV1} after publishing them.
+ *
+ * The first domain a project adds LOCKS the project's SES `region`; every
+ * later domain must match it. `stream_default` requires `stream`, and sending
+ * it alone is answered with `422 validation_error` rather than ignored.
+ */
+ createV1(body: CreateDomainV1Request): Promise;
+ /** Retrieve a single sending domain. */
+ getV1(id: string): Promise;
+ /**
+ * Re-read the domain's state from SES and DNS, and resolve the refreshed
+ * document.
+ *
+ * This does not verify anything and changes none of the domain's own fields.
+ * Verification happens in the domain's DNS, when its owner publishes the DKIM
+ * records SES minted at creation, and Amazon decides when those resolve. What
+ * this call does is ask SES what it currently sees, re-check SPF and DMARC,
+ * and persist that answer — so a caller polling after a DNS change learns the
+ * outcome without waiting for the periodic sweep. Calling it on a domain whose
+ * records are not published yet is not an error and does not hurry anything.
+ *
+ * A POST rather than a GET because the refreshed state is persisted and a
+ * verified/unverified transition notifies the project.
+ */
+ verifyV1(id: string): Promise;
+ /**
+ * Remove a sending domain. Resolves `{ id, deleted }`.
+ *
+ * Refused with `409 conflict` while a template, workflow step or active
+ * campaign still sends from an address on this host. The SES identity goes
+ * too unless another project holds the same host — and its DKIM keys with it,
+ * so re-adding later mints records that must be published again.
+ */
+ deleteV1(id: string): Promise;
}
declare class EmailsResource {
@@ -10301,10 +17582,24 @@ declare class EmailsResource {
batch(body: BatchSendRequest, opts?: IdempotencyOptions): Promise;
/** List emails with cursor-based pagination + filters. */
list(query?: ListEmailsQuery): Promise;
- /** Fetch a single email and its delivery events. */
- get(id: string): Promise;
- /** Cancel a scheduled (PENDING) email before it fires. */
- cancelSchedule(id: string): Promise;
+ /**
+ * Fetch a single email together with its DELIVERY history, oldest first.
+ *
+ * `events` here is the delivery timeline behind `status` — not the custom
+ * events recorded with `events.record`, which are read from `events.list`.
+ * Before 1.1 this operation answered the wrong relation and published the
+ * message's dedup and idempotency ledger keys along with it.
+ */
+ get(id: string): Promise;
+ /**
+ * Cancel a scheduled (PENDING) email before it fires.
+ *
+ * Resolves the email itself, not an empty acknowledgement: the contract has
+ * always published `EmailResponse` here, and the caller wants the row's new
+ * status more than it wants a `{ success: true }` it already inferred from the
+ * absence of an exception.
+ */
+ cancelSchedule(id: string): Promise;
}
/**
@@ -10360,8 +17655,14 @@ declare class EventsResource {
}
/**
- * Subscription management for a mailing list, on the legacy `/api/*` surface
- * (envelope responses, camelCase — the SDK unwraps to `data`).
+ * Subscriber lists, on both surfaces.
+ *
+ * {@link subscribe} and {@link unsubscribe} speak the legacy `/api/*` dialect
+ * (camelCase inside a `{ success, data }` envelope the SDK unwraps) and accept
+ * SENDING_ONLY keys. The `V1`-suffixed methods manage the lists themselves on
+ * `/api/v1`: bare snake_case bodies and RFC 9457 problem documents. Both
+ * dialects describe the same lists, so the suffix is there to keep a call site
+ * from confusing one for the other.
*/
declare class ListsResource {
private readonly client;
@@ -10373,8 +17674,8 @@ declare class ListsResource {
* **Double opt-in.** When the list has `doubleOptIn` enabled the membership
* is created as `PENDING` and the result carries a `confirmToken`. Sendly
* does **not** send the confirmation email — your application must deliver
- * `/api/lists/confirm?token=` to the contact itself. The token
- * is valid for 24 hours.
+ * `/api/lists/confirm-subscription?token=` to the contact
+ * itself. The token is valid for 24 hours.
*
* **Re-subscribing after an opt-out.** If the email already holds an
* `UNSUBSCRIBED` membership on this list, the call fails with
@@ -10390,22 +17691,70 @@ declare class ListsResource {
subscribe(id: string, body: ListSubscribeRequest): Promise;
/** Unsubscribe a contact from a list. Resolves the address that was removed. */
unsubscribe(id: string, body: ListUnsubscribeRequest): Promise;
+ /**
+ * List the project's subscriber lists on the `/api/v1` surface.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold the
+ * arguments steady for the whole walk — changing them mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ listV1(query?: ListListsV1Query): Promise;
+ /** Iterate every list across pages, yielding one list at a time. */
+ listAllV1(query?: ListListsV1Query): AsyncGenerator;
+ /**
+ * Create a list. Only `name` is required; `double_opt_in` defaults to false.
+ *
+ * Turning double opt-in on does not make Sendly send anything — it only
+ * changes {@link subscribe} to create the membership as `PENDING` and hand
+ * back the `confirmToken` your application delivers.
+ */
+ createV1(body: CreateListV1Request): Promise;
+ /**
+ * Retrieve a single list. `member_count` counts memberships in *any* status,
+ * `PENDING` and `UNSUBSCRIBED` included, so it is not the size of the
+ * audience a campaign would reach.
+ */
+ getV1(id: string): Promise;
+ /**
+ * Patch a list's name, description, opt-in mode, confirmation template, or
+ * redirect URL. Only the fields you send are changed; `member_count` is
+ * derived and never accepted here.
+ */
+ updateV1(id: string, body: UpdateListV1Request): Promise;
+ /** Delete a list. Resolves `{ id, deleted }`. Removes the list, not its contacts. */
+ deleteV1(id: string): Promise;
+ /**
+ * Start a bulk address-validation run over the list's members.
+ *
+ * **Billed per address checked**, so starting a run over a large list costs
+ * real money every time — it is not a free refresh. Answers `202` with the
+ * run in `pending`; read its progress and counts back with
+ * `validation.getRun`.
+ */
+ startValidationRun(id: string): Promise;
}
+/** What {@link MailboxesResource.sendMessage} resolves once the message is submitted. */
+type MailboxMessageSubmitted = paths["/api/mailboxes/{id}/messages"]["post"]["responses"][201]["content"]["application/json"]["data"];
+/** What {@link MailboxesResource.draftMessage} resolves — suggested text, and `sent: false`. */
+type MailboxMessageDraft = paths["/api/mailboxes/{id}/drafts"]["post"]["responses"][200]["content"]["application/json"]["data"];
/**
- * Receiving mailboxes on the project's verified domains.
+ * Receiving mailboxes on the project's verified domains, plus the two
+ * composition operations an API key may drive.
*
- * READ ONLY, and deliberately so. Creating and deleting a mailbox, and minting
- * or revoking an app password, all resolve the acting project admin from the
- * session user; an API key carries no user, so those routes answer `401` to any
- * `sk_` key however broad its scopes. The contract records that — they publish
- * `SessionAuth` without `ApiKeyAuth` — and this SDK authenticates only with API
- * keys, so a `create`/`delete` here could never succeed. They are listed in the
- * contract suite's `NOT_SDK_CALLABLE` rather than shipped as methods that
- * always throw.
+ * MAILBOX LIFECYCLE is what stays out of reach: creating and deleting a
+ * mailbox, and minting or revoking an app password, all resolve the acting
+ * project admin from the session user; an API key carries no user, so those
+ * routes answer `401` to any `sk_` key however broad its scopes. The contract
+ * records that — they publish `SessionAuth` without `ApiKeyAuth` — and this SDK
+ * authenticates only with API keys, so a `create`/`delete` here could never
+ * succeed. They are listed in the contract suite's `NOT_SDK_CALLABLE` rather
+ * than shipped as methods that always throw.
*
- * The three reads below are a different case: their membership check is
- * conditional, so a key really can call them.
+ * Everything below is a different case — the reads' membership check is
+ * conditional, and {@link sendMessage} / {@link draftMessage} publish
+ * `ApiKeyAuth` outright — so a key really can call them.
*/
declare class MailboxesResource {
private readonly client;
@@ -10441,6 +17790,43 @@ declare class MailboxesResource {
* this can identify a credential without being able to reconstruct it.
*/
listAppPasswords(id: string): Promise;
+ /**
+ * SENDS a new message — real mail leaves the account, from the mailbox in the
+ * path, over its own domain, and the recipient can reply to it.
+ *
+ * There is no `from` field, on purpose: a route that sends under a customer's
+ * own identity must not take that identity as an argument. `body` is plain
+ * text and HTML is refused — Sendly renders the HTML part itself, escaping as
+ * it goes, so text becomes markup in exactly one place.
+ *
+ * Bcc recipients are delivered to but appear in no header, so the copy filed
+ * in the mailbox's Sent folder does not record them. The message is stored as
+ * a new conversation, and the reply threads onto it.
+ *
+ * Refusals worth handling by name: `422 RECIPIENT_SUPPRESSED` (a recipient is
+ * on the project's suppression list), `422 CONTENT_REFUSED` (the outbound
+ * scanner declined it), `503 CONTENT_SCAN_UNAVAILABLE` (no verdict yet for a
+ * young project — nothing was sent, retry shortly). A mailbox may send 60
+ * messages an hour here.
+ */
+ sendMessage(id: string, body: ComposeMailboxMessageRequest): Promise;
+ /**
+ * SENDS NOTHING — asks Sendly's assistant to write text for this mailbox and
+ * hands it back for you to review. The response always reports `sent: false`,
+ * and no argument changes that.
+ *
+ * `mode` picks the job: `draft` writes a new email from a brief, `rewrite`
+ * reworks text you already have, `subject` returns alternative subject lines
+ * in `subjects`. The mailbox is named only so the text can be written in that
+ * address's voice; no correspondence is read and nothing is stored.
+ *
+ * That is why this asks only for `mailboxes:read` while {@link sendMessage}
+ * needs `mailboxes:send` — a client that may draft is not thereby a client
+ * that may mail your customers. Everything you pass is treated strictly as
+ * data describing what to write, never as instructions to the model. Capped
+ * at 120 requests an hour per project; `502` means the model was unreachable.
+ */
+ draftMessage(id: string, body: DraftMailboxMessageRequest): Promise;
}
/**
@@ -10502,32 +17888,245 @@ declare class SegmentsResource {
listContactsAll(id: string, query?: ListSegmentContactsV1Query): AsyncGenerator;
}
+/**
+ * Snippets — reusable body fragments a template pulls in with `{{> name}}`.
+ *
+ * Legacy dialect: `{ success, data }` envelopes and camelCase fields. Gated by
+ * the same `templates:*` scopes as the templates that include them, because a
+ * snippet is part of a template body rather than a resource with an audience of
+ * its own.
+ */
+declare class SnippetsResource {
+ private readonly client;
+ constructor(client: Sendly);
+ /**
+ * Create a snippet. `name` is the literal identifier templates include with
+ * `{{> name}}` and is unique within the project, so a clash answers 409.
+ */
+ create(body: CreateSnippetRequest): Promise;
+ /** List snippets with cursor pagination (`limit`/`cursor`) + optional `search` over name and description. */
+ list(query?: ListSnippetsQuery): Promise;
+ /** Fetch a single snippet by id. */
+ get(id: string): Promise;
+ /** Patch an existing snippet. */
+ update(id: string, body: UpdateSnippetRequest): Promise;
+ /**
+ * Delete a snippet. The API answers 200 with `{ success, data: { id } }`; the
+ * SDK resolves void. Templates that still include it keep rendering — an
+ * absent snippet renders as an empty string, like an absent variable.
+ */
+ delete(id: string): Promise;
+}
+
+/**
+ * The project suppression list — the addresses no send may reach — in both
+ * dialects.
+ *
+ * The unsuffixed methods speak legacy `/api/suppression` (singular path,
+ * `{ success, data }` envelopes); the `V1` methods speak `/api/v1/suppressions`
+ * (plural path, bare bodies, RFC 9457 problem documents). Both answer the same
+ * question, so the suffix is what stops a call site from reaching for one and
+ * reading the other's shape.
+ */
declare class SuppressionResource {
private readonly client;
constructor(client: Sendly);
/** Add an email to the project suppression list. */
add(body: AddSuppressionRequest): Promise;
- /** List suppressions with optional reason filter + cursor pagination. */
+ /**
+ * List suppressions with optional reason filter + cursor pagination.
+ *
+ * Alone among the legacy reads, this route answers no `{ success, data }`
+ * envelope: the page IS the body, `{ items, nextCursor }`, so nothing is
+ * unwrapped. Each record carries `scope` — `PROJECT` for every record this
+ * API creates or returns today.
+ */
list(query?: ListSuppressionsQuery): Promise;
/** Check whether a given email is suppressed. */
get(email: string): Promise;
/** Remove an email from the suppression list. Returns 204. */
remove(email: string): Promise;
+ /**
+ * List suppressed addresses, newest first.
+ *
+ * Cursor-paginated on `limit` + `after`, with no total count. Hold `reason`
+ * steady for the whole walk — changing it mid-pagination returns
+ * `422 validation_error` asking you to restart. {@link listAllV1} drives the
+ * loop for you.
+ */
+ listV1(query?: ListSuppressionsV1Query): Promise;
+ /** Iterate every suppressed address across pages, yielding one record at a time. */
+ listAllV1(query?: ListSuppressionsV1Query): AsyncGenerator;
+ /**
+ * Suppress an address, so no further send reaches it.
+ *
+ * Idempotent: an already-suppressed address answers `201` with the existing
+ * record, and the first `reason` wins — a later manual entry must not
+ * overwrite what an SES bounce recorded. `source` is not accepted in the
+ * body; it is derived from the credential, so a record's provenance cannot be
+ * dressed up as a deliverability fact.
+ */
+ createV1(body: CreateSuppressionV1Request): Promise;
+ /**
+ * Retrieve the suppression record for one address.
+ *
+ * The answer is definite either way: `200` means suppressed and says why,
+ * `404 resource_not_found` means the address is not on the list. A `200` may
+ * also come from a platform-wide block recorded outside this project.
+ */
+ getV1(email: string): Promise