> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduled reports: cadence, recipients, and the send-now path

> Set up email-delivered analytics reports on a daily, weekly, or monthly cadence — how the 09:00 local tick is computed, how to manage recipients, what send-now actually does, and who can manage reports.

# Scheduled reports: cadence, recipients, and the send-now path

A scheduled report is an analytics rollup Orbit emails to a fixed recipient
list on a recurring cadence. Each report has a **name**, a report **type**
(the metrics section it contains), a **frequency** (`daily`, `weekly`, or
`monthly`), a **recipients** list of 1–50 email addresses, and an optional
IANA **timezone** the cadence anchors to. The report arrives as an email
with the metrics rendered in the body and a PDF attached.

Report types:

| Type               | Contents                                                                                                   |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| `messaging_volume` | Send/delivery volume across channels                                                                       |
| `deliverability`   | Delivery, read, and click rates                                                                            |
| `top_contacts`     | Highest-traffic contacts in the window                                                                     |
| `spend`            | Cost breakdown by channel and destination                                                                  |
| `custom`           | A saved report-builder query (queued performance CCaaS data when `filters.dataset` is `queue_performance`) |

All scheduled-report endpoints live under `/api/v1/analytics/scheduled-reports`.
This is the **only** write surface for scheduled reports — the older
`/api/v1/reports/scheduled` CRUD was removed; any report it created was
migrated automatically, so nothing was lost.

## Create a report

Create one with `POST /api/v1/analytics/scheduled-reports`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/analytics/scheduled-reports \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly deliverability digest",
    "type": "deliverability",
    "frequency": "weekly",
    "recipients": ["ops@example.com", "growth@example.com"],
    "timezone": "America/New_York",
    "enabled": true
  }'
```

* `recipients` accepts an array of emails or one comma-separated string;
  duplicates (case-insensitive) are dropped. Every entry must be a valid
  email, and the list is capped at 50 addresses. Expect a `400`
  (`VALIDATION_ERROR`) when a recipient is malformed.
* `timezone` is optional. When set, the cadence anchor is validated as an
  IANA zone name (`America/New_York`, `Asia/Tehran`, …). When omitted, the
  cadence anchors to UTC.
* `enabled` defaults to `true`; create with `"enabled": false` to stage a
  report without sending it.
* `filters` is an optional JSON object. For `custom` reports it carries the
  saved-query definition, and `filters.dataset` must be `messages` or
  `queue_performance` when present.

## How the cadence tick is computed

Every report fires at **09:00 local time** — never at the moment you created
it, and never a raw interval like `NOW() + 30 days`:

* `daily` → next day at 09:00
* `weekly` → the start of next week at 09:00
* `monthly` → the first day of the next month at 09:00

Anchoring to calendar boundaries matters: an interval-based monthly schedule
drifts by several days per year, and a monthly report created on the 31st
would slowly migrate earlier in the month. Anchoring keeps a monthly report
on the 1st and a weekly report on the same weekday, indefinitely.

When the stored `next_send_at` instant has already arrived — for example
right after a send-now request, or in the short window between a tick
becoming due and the scheduler claiming it — the API does **not** echo that
past/now value back. Because send-now works by marking the report due, the
raw value would otherwise read as "now" (or a past instant) even though the
recurring schedule didn't change. Instead the API returns the *upcoming*
cadence tick, so the value you read (and the dashboard renders) is always
the next real 09:00 tick, never a transient due-time.

## Edit cadence, recipients, or sections

`PATCH /api/v1/analytics/scheduled-reports/{id}` accepts any subset of
fields; omitted fields keep their current values. The endpoint requires at
least one field.

```bash theme={null}
curl -X PATCH https://api.orbit.devotel.io/api/v1/analytics/scheduled-reports/sr_01H… \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "frequency": "monthly", "recipients": ["ops@example.com"] }'
```

Two update behaviors are worth knowing:

* **Frequency changes re-anchor the schedule.** When you change `frequency`,
  the next tick is recomputed from the last send (or from now if the report
  never sent), floored so the result can never be in the past. So switching
  a stale weekly report to daily makes the next tick *tomorrow at 09:00* —
  not next week minus the elapsed days. It also never triggers an immediate
  send as a side effect.
* **Recipient edits replace the list.** `recipients` is replaced wholesale,
  not merged — pass the full final list on every update. The same 1–50
  unique-valid-email rules apply.

## Send-now: what it does and doesn't do

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/analytics/scheduled-reports/sr_01H…/send-now \
  -H "X-API-Key: $ORBIT_API_KEY"
```

`send-now` marks the report **due**; the scheduler worker picks it up on its
next tick and delivers it. It does **not** build and send the email inside
your HTTP request — the response returns immediately with
`{ "queued": true }`, keeping send/retry logic out of the API tier. Poll
`GET /api/v1/analytics/scheduled-reports` and watch `last_sent_at` to see
when it actually lands.

It also doesn't move the cadence. Because the list endpoint presents the
upcoming 09:00 tick whenever a report is due, a daily report keeps showing
"tomorrow 09:00" after send-now rather than collapsing to "a few minutes
from now."

## Delete

```bash theme={null}
curl -X DELETE https://api.orbit.devotel.io/api/v1/analytics/scheduled-reports/sr_01H… \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Deleting is permanent and returns `{ "id": "sr_01H…", "deleted": true }`.
Deleting stops all future sends; previously sent emails are of course
unaffected. To pause a report without losing its recipient list and
schedule, set `"enabled": false` on a PATCH instead of deleting it.

## Role gate

Reads (`GET`) are open to any authenticated member of the organization.
Writes — create, update, delete, and send-now — require the **owner** or
**admin** role. A scheduled report emails recipient-level metrics (top
contacts, spend, error breakdowns) to an arbitrary list of addresses the
creator chooses: that is a data-egress path, so it's restricted to roles
trusted to export org data. If viewer-role tokens could create reports, a
read-only member could exfiltrate the same analytics to any mailbox. The
role check comes back as a `403` on write endpoints for developer/viewer
keys and members.

## Troubleshooting

| Symptom                                                                  | Why / fix                                                                                                                                                                                                                                                |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A recipient doesn't get the email                                        | Recipients are replaced, not merged — a later PATCH without that address removed it. GET the report, confirm the address is in `recipients`, and check that recipient's mail filters/spam.                                                               |
| `next_send_at` looks "past" right after a PATCH that changed `frequency` | The re-anchor is floored at now, so a past instant should never be stored — but the value shown during the due window is always the *next* 09:00 tick. Re-read the row; if it persists past the next scheduler tick, contact support with the report ID. |
| Report created today didn't send today                                   | Cadence ticks are the next calendar boundary at 09:00 local (tomorrow for daily, next week for weekly, the 1st for monthly) — a report never fires the moment it's created. Use send-now for the first delivery.                                         |
| `400` on create/update                                                   | A recipient isn't a valid email, the list has 0 or >50 unique addresses, `timezone` isn't an IANA name, `filters.dataset` is unknown, or a PATCH had no fields. The response body names the failing field.                                               |
| `403` on create/update/delete/send-now                                   | The key or member has a developer/viewer role. Writes need owner/admin — see the role gate above.                                                                                                                                                        |
| `404` on update/delete/send-now                                          | The report ID doesn't exist in your organization (already deleted, or the ID is from another org).                                                                                                                                                       |
| Send-now returned `queued` but no email arrived                          | The scheduler delivers on its next tick; watch `last_sent_at` on a GET. If it hasn't moved after several minutes, contact support with the report ID.                                                                                                    |

## See also

* [Analytics API reference](/api-reference/analytics) — endpoint table for
  `/api/v1/analytics/scheduled-reports`
* [Production go-live checklist](/guides/go-live-checklist) — launch gates
  before live traffic
