Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Sendly Now Deliverability API
version: 1.0.0
contact:
name: Sendly Support
url: https://sendly.now
license:
name: AGPL-3.0
url: https://www.gnu.org/licenses/agpl-3.0.txt
description: 'Operations tagged Deliverability across 2 of this provider''s published API definitions: openapi.json, sendly-now-openapi.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.sendly.now
tags:
- description: 'Why mail from one of your domains is or is not arriving: DNS identity, recent delivery outcomes, and one recipient''s suppression state, composed into findings.'
name: Deliverability
paths:
/api/v1/deliverability/diagnose:
get:
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.
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.'
operationId: v1DiagnoseDeliverability
parameters:
- description: A sending domain in this project, e.g. `example.com`.
in: query
name: domain
required: true
schema:
description: A sending domain in this project, e.g. `example.com`.
maxLength: 253
minLength: 3
type: 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.
in: query
name: address
required: false
schema:
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.
format: email
type: string
- description: How far back the delivery counters look. 1–30 days; defaults to 7.
in: query
name: window_days
required: false
schema:
description: How far back the delivery counters look. 1–30 days; defaults to 7.
maximum: 30
minimum: 1
type: integer
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/DeliverabilityDiagnosisV1'
description: The diagnosis, with findings
'401':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`invalid_api_key` or `invalid_session` — missing or invalid credentials.'
'403':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`scope_missing`, `project_access_denied`, or `project_disabled`.'
'422':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`validation_error` — query, path, or body parameters did not match the schema.'
'429':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`rate_limited` — see `Retry-After` and the `RateLimit` headers.'
'500':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`internal_error`.'
security:
- ApiKeyAuth: []
- SessionAuth: []
- OAuth2:
- deliverability:read
summary: Diagnose why mail from a domain is not arriving
tags:
- Deliverability
servers:
- url: https://api.sendly.now
/api/v1/deliverability/dmarc:
get:
description: 'DMARC aggregate (RUA) reports receiving providers have sent about your verified domains, newest reporting window first.
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.
`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.'
operationId: v1ListDmarcReports
parameters:
- in: query
name: limit
required: false
schema:
default: 20
maximum: 100
minimum: 1
type: integer
- description: Opaque cursor from a previous response's `next_cursor`.
in: query
name: after
required: false
schema:
description: Opaque cursor from a previous response's `next_cursor`.
minLength: 1
type: string
- description: How far back to read, by the report's window start. 1-180 days; defaults to 30.
in: query
name: days
required: false
schema:
description: How far back to read, by the report's window start. 1-180 days; defaults to 30.
maximum: 180
minimum: 1
type: integer
- description: Restrict to reports about one of your domains.
in: query
name: domain
required: false
schema:
description: Restrict to reports about one of your domains.
maxLength: 253
minLength: 1
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/DmarcReportV1List'
description: Cursor-paginated DMARC aggregate reports
'401':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`invalid_api_key` or `invalid_session` — missing or invalid credentials.'
'403':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`scope_missing`, `project_access_denied`, or `project_disabled`.'
'422':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`validation_error` — query, path, or body parameters did not match the schema.'
'429':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`rate_limited` — see `Retry-After` and the `RateLimit` headers.'
'500':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`internal_error`.'
security:
- ApiKeyAuth: []
- SessionAuth: []
- OAuth2:
- deliverability:read
summary: DMARC aggregate reports for your domains
tags:
- Deliverability
servers:
- url: https://api.sendly.now
/api/v1/deliverability/domains:
get:
description: 'Sent, delivered, bounced, complained and opened counts split by the RECIPIENT''s domain and by UTC day, newest day first.
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.
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 `deliverability:read` scope — Check why mail from one of your domains is not arriving.'
operationId: v1ListRecipientDomainStats
parameters:
- in: query
name: limit
required: false
schema:
default: 20
maximum: 100
minimum: 1
type: integer
- description: Opaque cursor from a previous response's `next_cursor`.
in: query
name: after
required: false
schema:
description: Opaque cursor from a previous response's `next_cursor`.
minLength: 1
type: string
- description: How far back to read. 1-30 days; defaults to 30, which is the window the job maintains.
in: query
name: days
required: false
schema:
description: How far back to read. 1-30 days; defaults to 30, which is the window the job maintains.
maximum: 30
minimum: 1
type: integer
- description: Restrict to one recipient domain.
in: query
name: domain
required: false
schema:
description: Restrict to one recipient domain.
maxLength: 253
minLength: 1
type: string
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/RecipientDomainStatsV1List'
description: Cursor-paginated recipient-domain rollup
'401':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`invalid_api_key` or `invalid_session` — missing or invalid credentials.'
'403':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`scope_missing`, `project_access_denied`, or `project_disabled`.'
'422':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`validation_error` — query, path, or body parameters did not match the schema.'
'429':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`rate_limited` — see `Retry-After` and the `RateLimit` headers.'
'500':
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
description: '`internal_error`.'
security:
- ApiKeyAuth: []
- SessionAuth: []
- OAuth2:
- deliverability:read
summary: Delivery outcomes per recipient domain
tags:
- Deliverability
servers:
- url: https://api.sendly.now
components:
schemas:
DmarcReportV1:
description: One DMARC aggregate (RUA) report.
properties:
fail_count:
type: integer
id:
type: string
org_name:
description: The reporting receiver, e.g. `google.com`.
type: string
pass_count:
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.'
type: integer
policy_domain:
description: The domain of yours the report is about.
type: string
range_begin:
format: date-time
type: string
range_end:
format: date-time
type: string
received_at:
format: date-time
type: string
report_id:
description: The receiver's own id for this report.
type: string
sources:
description: Per-sending-source rows, as the receiver reported them.
items:
properties:
count:
type: integer
disposition:
type: string
dkim:
type: string
header_from:
type: string
source_ip:
type: string
spf:
type: string
required:
- source_ip
- count
- disposition
- dkim
- spf
- header_from
type: object
type: array
total_count:
type: integer
required:
- id
- report_id
- org_name
- policy_domain
- range_begin
- range_end
- total_count
- pass_count
- fail_count
- sources
- received_at
type: object
Problem:
description: RFC 9457 problem document, served as `application/problem+json`. Returned by every 4xx/5xx response on the `/api/v1` surface.
properties:
code:
description: Machine-readable lowercase error code, e.g. `scope_missing`.
type: string
detail:
description: Explanation specific to this occurrence.
type: string
errors:
description: Field-level failures. Present on 422 `validation_error` responses.
items:
properties:
code:
type: string
message:
type: string
pointer:
description: RFC 6901 JSON Pointer to the offending field.
type: string
required:
- pointer
- code
- message
type: object
type: array
instance:
description: Request path the failure occurred on.
type: string
next:
description: The tool an agent calls next to repair this failure — `request_scopes` for a missing permission, `upgrade_plan` for an exhausted plan. Absent when no tool repairs it.
properties:
args:
additionalProperties: {}
description: Arguments to call it with.
type: object
tool:
description: The MCP tool that repairs this failure, e.g. `upgrade_plan`.
type: string
required:
- tool
type: object
request_id:
description: Correlation id — quote it in support requests.
type: string
status:
description: HTTP status code, repeated in the body.
type: integer
title:
description: Short, stable summary — the same for every occurrence of a `type`.
type: string
type:
description: Dereferenceable URI identifying the error class, anchored on the docs errors page.
format: uri
type: string
required:
- type
- title
- status
- code
type: object
DeliverabilityFindingSeverityV1:
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:
- blocking
- degraded
- info
type: string
DeliverabilityFindingV1:
description: One diagnosed problem, with its fix.
properties:
code:
description: Stable identifier for this finding, e.g. `domain_not_verified`. Branch on this, not on `summary`.
type: string
remedy:
description: What to do about it.
type: string
severity:
$ref: '#/components/schemas/DeliverabilityFindingSeverityV1'
summary:
description: What is wrong, in one sentence.
type: string
required:
- code
- severity
- summary
- remedy
type: object
DeliverabilityDiagnosisV1:
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.'
properties:
address:
type:
- string
- 'null'
checked_at:
format: date-time
type: string
domain:
type: string
findings:
description: What is wrong, worst first. An empty array means nothing here explains a delivery problem.
items:
$ref: '#/components/schemas/DeliverabilityFindingV1'
type: array
identity:
$ref: '#/components/schemas/DeliverabilityIdentityV1'
recent_delivery:
$ref: '#/components/schemas/DeliverabilityRecentDeliveryV1'
suppression:
$ref: '#/components/schemas/DeliverabilitySuppressionV1'
required:
- domain
- address
- checked_at
- identity
- suppression
- recent_delivery
- findings
type: object
DeliverabilitySuppressionV1:
description: Null unless the request named an `address`.
properties:
reason:
enum:
- HARD_BOUNCE
- COMPLAINT
- MANUAL
- UNSUBSCRIBE
- null
type:
- string
- 'null'
source:
enum:
- SES_WEBHOOK
- API
- DASHBOARD
- null
type:
- string
- 'null'
suppressed:
type: boolean
suppressed_at:
format: date-time
type:
- string
- 'null'
required:
- suppressed
- reason
- source
- suppressed_at
type:
- object
- 'null'
RecipientDomainStatsV1:
description: Delivery outcomes for one recipient domain on one day.
properties:
bounced:
type: integer
complained:
type: integer
computed_at:
description: When the rollup job last rebuilt this row. These counts are a CACHE, refreshed hourly.
format: date-time
type: string
day:
description: The UTC day these counts cover, as `YYYY-MM-DD`.
type: string
delivered:
type: integer
domain:
description: 'The recipient''s domain, lowercased: the part after the `@`.'
type: string
opened:
type: integer
sent:
type: integer
required:
- domain
- day
- sent
- delivered
- bounced
- complained
- opened
- computed_at
type: object
DeliverabilityRecentDeliveryV1:
description: Delivery outcomes over the requested window.
properties:
bounce_rate:
description: Bounced ÷ sent (0–1), or null when nothing was sent in the window.
type:
- number
- 'null'
bounced:
type: integer
complained:
type: integer
complaint_rate:
type:
- number
- 'null'
delivered:
type: integer
failed:
type: integer
scope:
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:
- project
type: string
sent:
type: integer
window_days:
type: integer
required:
- window_days
- scope
- sent
- delivered
- bounced
- complained
- failed
- bounce_rate
- complaint_rate
type: object
DeliverabilityIdentityV1:
description: The sending identity's DNS health, as last refreshed.
properties:
dkim_status:
description: DKIM signing. This is the one that decides whether Sendly will send from the domain at all.
enum:
- NOT_CHECKED
- PENDING
- VERIFIED
- FAILED
- null
type:
- string
- 'null'
dmarc_status:
description: The DMARC policy published at `_dmarc.<domain>`.
enum:
- NOT_CHECKED
- PENDING
- VERIFIED
- FAILED
- null
type:
- string
- 'null'
last_checked_at:
description: When the DNS refresh job last looked. These statuses are a CACHE, not a live lookup.
format: date-time
type:
- string
- 'null'
mail_from_domain:
type:
- string
- 'null'
mail_from_domain_status:
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.
type:
- string
- 'null'
mx_status:
description: Inbound receiving only. Null unless the domain has receiving enabled.
enum:
- NOT_CHECKED
- PENDING
- VERIFIED
- FAILED
- null
type:
- string
- 'null'
registered:
description: Whether this project has a domain record at all. False makes every other field null.
type: boolean
spf_status:
description: SPF alignment for the sending identity.
enum:
- NOT_CHECKED
- PENDING
- VERIFIED
- FAILED
- null
type:
- string
- 'null'
verified:
type: boolean
required:
- registered
- verified
- dkim_status
- spf_status
- dmarc_status
- mx_status
- mail_from_domain
- mail_from_domain_status
- last_checked_at
type: object
DmarcReportV1List:
description: Cursor-paginated DMARC aggregate reports, newest window first.
properties:
data:
items:
$ref: '#/components/schemas/DmarcReportV1'
type: array
has_more:
type: boolean
intake_configured:
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.
type: boolean
next_cursor:
description: Pass as `after` to fetch the next page. `null` on the last page.
type:
- string
- 'null'
required:
- data
- has_more
- next_cursor
- intake_configured
type: object
RecipientDomainStatsV1List:
description: Cursor-paginated recipient-domain rollup, newest day first.
properties:
data:
items:
$ref: '#/components/schemas/RecipientDomainStatsV1'
type: array
has_more:
type: boolean
next_cursor:
description: Pass as `after` to fetch the next page. `null` on the last page.
type:
- string
- 'null'
required:
- data
- has_more
- next_cursor
type: object
securitySchemes:
ApiKeyAuth:
bearerFormat: API Key
description: 'API key authentication. Use a `sk_*` (FULL) or `pk_*` (SENDING_ONLY) key as the bearer token. Public keys (`pk_*`) are restricted to the email-send endpoints and the self-serve list subscribe/unsubscribe pair; every other endpoint — event tracking included — answers 403 for them. In scope terms (used by `/api/v1/*` operations): FULL keys hold every scope; SENDING_ONLY keys hold only `emails:send`.'
scheme: bearer
type: http
MailgunBasicAuth:
description: 'Mailgun-style authentication for the `/api/compat/mailgun/*` operations only: HTTP Basic with the username `api` and a Sendly API key (`sk_*`, or a `pk_*` send-only key) as the password, exactly as mailgun.js sends it.'
scheme: basic
type: http
OAuth2:
description: 'OAuth 2.1 with PKCE, for AI agents and other delegated clients (this is what the MCP endpoint at `/api/mcp` uses, and how a third-party application sends on a user''s behalf). Tokens are minted through the consent screen and carry ONLY the scopes the user ticked there, so an operation lists the single scope it requires and a token without it answers `403` with code `SCOPE_MISSING` — before any input is parsed. Unlike an API key, a delegated token reaches an operation only where the route itself declares a scope; every other route refuses it outright.
**Ask for the right audience.** Pass `resource=https://app.sendly.now/api` on the authorization AND the token request to receive a token for this REST API; `resource=https://app.sendly.now/api/mcp` is the MCP endpoint''s identifier. A request carrying no `resource` receives an OPAQUE token, which every endpoint here rejects. Each identifier publishes RFC 9728 metadata at `/.well-known/oauth-protected-resource/api` and `/.well-known/oauth-protected-resource/api/mcp`.
**Name the project on every call.** An OAuth grant belongs to a USER, not to a project — the same person may belong to several — so a delegated token carries no tenant and the project is chosen per request with the `x-project-id` header, checked against real membership. Omit it and a project-scoped operation answers `400`; send one the user does not belong to and it answers `403 PROJECT_ACCESS_DENIED`. List the projects a token may name with `GET /api/users/me/projects` (scope `projects:read`), which needs no header. API keys are unaffected: a key already names its project.'
flows:
authorizationCode:
authorizationUrl: https://app.sendly.now/api/auth/oauth2/authorize
refreshUrl: https://app.sendly.now/api/auth/oauth2/token
scopes:
account:read: See your profile and notification preferences
account:write: Edit your profile and preferences, export your data, delete your account
analytics:read: View your sending analytics and engagement metrics
api-keys:read: See which API keys exist, including what each one is allowed to do
api-keys:write: Create, rotate, and revoke API keys — these keep working even after you disconnect this app
billing:read: See your plan, usage, invoices and spending limits
billing:write: Start a checkout, change spending limits, open the billing portal, cancel or refund
campaigns:read: View your campaigns and their performance
campaigns:send: Send or schedule your campaigns to their audience
campaigns:write: Create, edit, and organize your campaigns
connections:read: See connected services such as Slack and connected AI apps
connections:write: Connect, re-verify and disconnect services
contacts:read: View your contacts and their custom fields
contacts:write: Create, update, and delete your contacts
deliverability:read: Check why mail from one of your domains is not arriving
domains:read: View your sending domains and their verification status
domains:write: Add and remove sending domains, and trigger verification
emails:read: View the emails you have sent and their delivery status
emails:send: Send emails from your verified domains
emails:test: Send test emails to your own address from the Sendly sandbox
events:read: View the custom events your application has recorded
events:write: Record custom events for your contacts
lists:read: View your subscriber lists and who is on them
lists:write: Create, rename, and delete your subscriber lists
mailbox-conversations:read: Read the conversations in your hosted mailboxes — every message and attachment people have sent you
mailbox-conversations:reply: Reply to conversations in your hosted mailboxes, from the mailbox's own address
mailboxes:read: View the mailboxes on your domains and their settings
mailboxes:send: Write and send new email from your hosted mailboxes, as that address
mailboxes:write: Create and delete mailboxes on your verified domains
members:read: See who belongs to your projects
members:write: Add and remove project members and change their roles
projects:read: View your projects and their settings
projects:write: Create, change, reset and delete projects on your account
segments:read: View your segments and who belongs to them
segments:write: Create, edit, and delete your segments
support:read: See your support tickets
support:write: Open support tickets and suspension appeals
suppression:read: View the addresses on your suppression list
suppression:write: Add and remove addresses on your suppression list
templates:read: View your email templates
templates:write: Create, edit, and delete your email templates
topics:read: View the topics you mail about and who is subscribed to each
topics:write: Create and edit topics, and change what your contacts are subscribed to — this decides who your campaigns reach
usage:read: View your usage totals and billing limits
validation:read: View your email validation runs and their results
validation:write: Check whether email addresses can receive mail — this is billed per address
webhooks:read: View your webhook endpoints and their delivery history
webhooks:write: Create, edit, and delete your webhook endpoints
workflows:read: View your automation workflows and their runs
workflows:write: Create, edit, enable, and delete your automation workflows
tokenUrl: https://app.sendly.now/api/auth/oauth2/token
type: oauth2
PostmarkServerToken:
description: 'Postmark-style authentication for the `/api/compat/postmark/*` operations only: a Sendly API key (`sk_*`, or a `pk_*` send-only key) sent in the `X-Postmark-Server-Token` header, exactly where the postmark SDK puts its server token.'
in: header
name: X-Postmark-Server-Token
type: apiKey
SessionAuth:
description: BetterAuth session cookie. Used by the dashboard / browser clients. When present, the active project is taken from the `x-project-id` header.
in: cookie
name: better-auth.session_token
type: apiKey
x-refined-from:
- openapi.json
- sendly-now-openapi.json