update-customer-subscription
dci update-customer-subscription [body] [flags]
Partially updates one dashboard, report, commitment, alert, or budget subscription with an application/merge-patch+json body (RFC 7396). The writable fields are recipients.emails, recipients.slackChannels, and, on a scheduled subscription, subscriptions. Any other field, recipients: null, or null for either recipient list is 422 validation_failed. A patch that sets neither recipients nor subscriptions is 422 validation_failed. A wrong Content-Type is 415. Recipient and schedule value rules are on CustomerSubscriptionRecipients, CustomerSubscriptionRecipientPatch, and CustomerSubscriptionScheduleInput.
On a scheduled subscription, subscriptions is the desired set of dashboard, report, and commitment subscriptions. A type in the body and not on the document is added. A type on the document and missing from the body is removed. A type in both is updated when source.id or the schedule differs, and nextAt is recalculated. A stored entry this API does not list stays on the subscription. An empty subscriptions array is 422 validation_failed; deleting the item is DELETE. A repeated type is 422 customers.duplicate_subscription_type. An omitted subscriptions array leaves the nested types unchanged.
Create does not accept alert or budget. GET returns them. On an alert or a budget, subscriptions is accepted only when it is the one stored entry with the same type and source.id and no schedule. Any other array is 422 validation_failed. Those items never gain a dashboard, report, or commitment subscription. Alerts and budgets keep their own APIs for create, delete, and full management. This PATCH is the one write this API makes on them, and it may replace their recipient lists. DELETE of an alert or a budget returns 409 customers.entry_is_resource.
A patch whose normalized values match the stored values returns 200 and does not write. The etag is unchanged.
Recipient changes are allowed when the caller can edit every key on the document, including a key the response did not show. Otherwise they are allowed only when the caller removes exactly their own email: no added address, no Slack change, and no subscription-set change. Any other recipient change is 403. The caller must be allowed to manage every added or changed scheduled type and to delete every removed type.
If-Match must contain the current strong ETag. The token only covers fields this API can write. Idempotency-Key is required. The same key, caller, customer, method, path, body, and If-Match replays the stored success response. A different body or If-Match is 422 idempotency_key_reused. A missing key is 400 idempotency_key_required. While the first request is in progress, another request with that key is 409 idempotency_request_in_progress. A 4xx response releases the key. dryRun=true does not reserve the key. It returns the would-be subscription with the current etag and writes nothing.
Reads are immediately consistent. After a completed update, a GET of that id returns the new state.
Pass the request body as name: value arguments or pipe JSON on stdin — see Command structure.
Request
Content-Type: application/merge-patch+json
| Field | Type | Required | Description |
|---|---|---|---|
recipients | object | Recipient lists to replace. An omitted list stays. | |
recipients.emails | array of string (email) | Replacement email list, in the order sent. The same rules as CustomerSubscriptionRecipients.emails apply to a new address. | |
recipients.slackChannels | array of string | Replacement Slack channel id list, in the order sent. The same rules as CustomerSubscriptionRecipients.slackChannels apply to a new id. | |
subscriptions | array of object | Desired nested subscriptions. The server sorts them by type ascending. Request order is not stored and is not significant. On a scheduled subscription, the scheduled set to store. On an alert or a budget, the one stored entry. | |
subscriptions[].type | string | yes | Nested subscription type. |
subscriptions[].source | object | yes | Source to attach, or the stored alert or budget id. |
subscriptions[].source.id | string | yes | ID of the dashboard, report, or commitment. Must belong to the tenant. Report ids come from GET /analytics/v1/reports and GET /analytics/v1/reports/\{id\}. Commitment ids come from GET /analytics/v1/commitment-manager and GET /analytics/v1/commitment-manager/\{id\}. Create checks that id against the customer's dashboards and the preset dashboards. |
subscriptions[].schedule | object | Delivery schedule. Required for a scheduled type. Omit it when echoing an alert or a budget. | |
subscriptions[].schedule.timeZone | string | yes | IANA time zone for this schedule. |
subscriptions[].schedule.startDate | string (date-time) | yes | When this schedule starts. The start day is the calendar date of this instant in timeZone, not its UTC date. To start on 2026-11-03 in America/Los_Angeles, send 2026-11-03T00:00:00-08:00 (2026-11-03T08:00:00Z). A read returns the stored instant as UTC RFC3339 (Z). |
subscriptions[].schedule.time | string (date-time) | yes | Delivery clock to store. The value is an instant. A read returns it as UTC RFC3339 (Z) and does not preserve an offset. The calendar date is stored and is not the start date. Deliveries use only the hour and minute of that instant, interpreted in timeZone. |
subscriptions[].schedule.frequency | string | yes | Delivery frequency to store. |
subscriptions[].schedule.customPeriodAmount | integer | Number of customPeriodUnit between deliveries. Required when frequency is Custom. At least 1. | |
subscriptions[].schedule.customPeriodUnit | string | Unit for customPeriodAmount. Required when frequency is Custom. | |
subscriptions[].attachments | array of string | Replacement attachment list for a report subscription. Omitted keeps the stored list. [] clears it. Rejected on any other type with 422. | |
subscriptions[].pdfSettings | object | Replacement PDF settings for a report subscription. Omitted keeps the stored settings. When sent, all three flags are required. Rejected on any other type with 422. | |
subscriptions[].pdfSettings.showFilters | boolean | yes | Include the report filters summary in the PDF. |
subscriptions[].pdfSettings.includeTable | boolean | yes | Include the data table in the PDF. |
subscriptions[].pdfSettings.includeAnnotations | boolean | yes | Include annotations in the PDF time range. |
Example body (JSON)
{
"recipients": {
"emails": [
]
}
}
Raw JSON schema
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"description": "JSON Merge Patch (RFC 7396) for one subscription. At least one of `recipients` or `subscriptions` is required. Omitted fields stay unchanged. `null` is rejected. Any other field is rejected.",
"properties": {
"recipients": {
"description": "Recipient lists to replace. An omitted list stays.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"description": "Recipient lists to replace. An included array replaces that list. An omitted array stays. `null` is rejected. An empty array clears that list. Both lists empty after the patch is `409` `customers.last_destination`.",
"properties": {
"emails": {
"type": "array",
"minItems": 0,
"description": "Replacement email list, in the order sent. The same rules as `CustomerSubscriptionRecipients.emails` apply to a new address.",
"items": {
"type": "string",
"format": "email",
"minLength": 1,
"x-doit-pii": true
},
"example": [
]
},
"slackChannels": {
"type": "array",
"minItems": 0,
"description": "Replacement Slack channel id list, in the order sent. The same rules as `CustomerSubscriptionRecipients.slackChannels` apply to a new id.",
"items": {
"type": "string",
"minLength": 1
},
"example": [
"C0123ABCD"
]
}
},
"example": {
"emails": [
]
}
}
]
},
"subscriptions": {
"type": "array",
"format": "set",
"minItems": 1,
"maxItems": 3,
"description": "Desired nested subscriptions. The server sorts them by `type` ascending. Request order is not stored and is not significant. On a scheduled subscription, the scheduled set to store. On an alert or a budget, the one stored entry.",
"items": {
"type": "object",
"additionalProperties": false,
"description": "One entry in a subscription-set patch. On a scheduled subscription, `type` is a scheduled type and `schedule` is required. On an alert or a budget, the array must be the one stored entry with the same `type` and `source.id` and no `schedule`. On a report subscription, an omitted `attachments` or `pdfSettings` stays. A sent `attachments` array replaces that list, including `[]` to clear. A sent `pdfSettings` object replaces all three flags and requires all three. A recipient-only patch must not send these fields. Changing them requires the same permission as a schedule change.",
"required": [
"type",
"source"
],
"properties": {
"type": {
"description": "Nested subscription type.",
"allOf": [
{
"type": "string",
"description": "Kind of subscription nested on one item. One id namespace is shared by a scheduled subscription, an alert, and a budget. Create accepts only `dashboard-subscription`, `report-subscription`, and `commitment-manager`. GET returns `alert` and `budget`. PATCH may replace their recipient lists, and that is the one write this API makes on them. Those lists are the same ones written by `PATCH /analytics/v1/alerts/{id}` and `PATCH /analytics/v1/budgets/{id}`. DELETE of an alert or a budget returns `409` `customers.entry_is_resource`. `EntryPatch.type` includes `alert` and `budget` so an echo of the stored entry validates.",
"example": "dashboard-subscription",
"enum": [
"alert",
"budget",
"commitment-manager",
"dashboard-subscription",
"report-subscription"
],
"x-enumDescriptions": {
"alert": "Alert entry. One item, and it cannot be deleted through this API.",
"budget": "Budget entry. One item, and it cannot be deleted through this API.",
"commitment-manager": "Commitment subscription. Scheduled delivery of a commitment in Commitment manager.",
"dashboard-subscription": "Dashboard subscription. Scheduled email or Slack delivery of a dashboard.",
"report-subscription": "Report subscription. Scheduled email or Slack delivery of a report."
}
}
],
"example": "dashboard-subscription"
},
"source": {
"description": "Source to attach, or the stored alert or budget id.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Source to attach. `id` is the same property GET returns. `name` is server-set and rejected.",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"description": "ID of the dashboard, report, or commitment. Must belong to the tenant. Report ids come from `GET /analytics/v1/reports` and `GET /analytics/v1/reports/{id}`. Commitment ids come from `GET /analytics/v1/commitment-manager` and `GET /analytics/v1/commitment-manager/{id}`. Create checks that id against the customer's dashboards and the preset dashboards.",
"example": "dashboard_123"
}
},
"example": {
"id": "dashboard_123"
}
}
]
},
"schedule": {
"description": "Delivery schedule. Required for a scheduled type. Omit it when echoing an alert or a budget.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Schedule fields the client sends. `nextAt` is server-set and rejected. `customPeriodAmount` and `customPeriodUnit` are required when `frequency` is `Custom` and rejected otherwise.",
"required": [
"timeZone",
"startDate",
"time",
"frequency"
],
"properties": {
"timeZone": {
"type": "string",
"minLength": 1,
"description": "IANA time zone for this schedule.",
"example": "Europe/Warsaw"
},
"startDate": {
"type": "string",
"format": "date-time",
"description": "When this schedule starts. The start day is the calendar date of this instant in `timeZone`, not its UTC date. To start on 2026-11-03 in `America/Los_Angeles`, send `2026-11-03T00:00:00-08:00` (`2026-11-03T08:00:00Z`). A read returns the stored instant as UTC RFC3339 (`Z`).",
"example": "2026-11-03T08:00:00Z"
},
"time": {
"type": "string",
"format": "date-time",
"description": "Delivery clock to store. The value is an instant. A read returns it as UTC RFC3339 (`Z`) and does not preserve an offset. The calendar date is stored and is not the start date. Deliveries use only the hour and minute of that instant, interpreted in `timeZone`.",
"example": "2026-10-23T08:00:00Z"
},
"frequency": {
"description": "Delivery frequency to store.",
"allOf": [
{
"type": "string",
"description": "Console delivery frequency. Wire values match the Console labels.",
"example": "Weekly",
"enum": [
"Daily",
"Weekly",
"Monthly",
"Annually",
"Custom"
],
"x-enumDescriptions": {
"Daily": "Deliver every day at the configured time.",
"Weekly": "Deliver once a week at the configured time.",
"Monthly": "Deliver once a month at the configured time.",
"Annually": "Deliver once a year at the configured time.",
"Custom": "Deliver on the interval in `customPeriodAmount` and `customPeriodUnit`."
}
}
],
"example": "Weekly"
},
"customPeriodAmount": {
"type": "integer",
"minimum": 1,
"description": "Number of `customPeriodUnit` between deliveries. Required when `frequency` is `Custom`. At least 1.",
"example": 2
},
"customPeriodUnit": {
"description": "Unit for `customPeriodAmount`. Required when `frequency` is `Custom`.",
"allOf": [
{
"type": "string",
"description": "Unit for a `Custom` frequency.",
"example": "Month",
"enum": [
"Day",
"Week",
"Month",
"Year"
],
"x-enumDescriptions": {
"Day": "The custom interval is a number of days.",
"Week": "The custom interval is a number of weeks.",
"Month": "The custom interval is a number of months.",
"Year": "The custom interval is a number of years."
}
}
],
"example": "Month"
}
},
"example": {
"timeZone": "America/Los_Angeles",
"startDate": "2026-11-03T08:00:00Z",
"time": "2026-11-03T16:00:00Z",
"frequency": "Weekly"
}
}
]
},
"attachments": {
"type": "array",
"uniqueItems": true,
"description": "Replacement attachment list for a report subscription. Omitted keeps the stored list. `[]` clears it. Rejected on any other type with `422`.",
"items": {
"type": "string",
"description": "File to attach to a report subscription email. Values may appear at most once. Attachments are email-only: Slack-only subscriptions may still store the choice, and delivery skips the files when there is no email recipient. An oversized file is omitted from that email with a note in the body. This API does not upload or return file bytes.",
"example": "pdf",
"enum": [
"pdf",
"csv"
],
"x-enumDescriptions": {
"pdf": "Attach a PDF of the report to the email.",
"csv": "Attach a CSV export of the report to the email."
}
},
"example": [
"csv"
]
},
"pdfSettings": {
"description": "Replacement PDF settings for a report subscription. Omitted keeps the stored settings. When sent, all three flags are required. Rejected on any other type with `422`.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "PDF rendering options for a report subscription. Present on every `report-subscription` read. A missing stored object reads as all `true`. The flags apply only when `pdf` is in `attachments`. They may still be stored while PDF is off.",
"required": [
"showFilters",
"includeTable",
"includeAnnotations"
],
"properties": {
"showFilters": {
"type": "boolean",
"description": "Include the report filters summary in the PDF.",
"example": true
},
"includeTable": {
"type": "boolean",
"description": "Include the data table in the PDF.",
"example": true
},
"includeAnnotations": {
"type": "boolean",
"description": "Include annotations in the PDF time range.",
"example": true
}
},
"example": {
"showFilters": true,
"includeTable": true,
"includeAnnotations": true
}
}
]
}
},
"example": {
"type": "dashboard-subscription",
"source": {
"id": "dashboard_123"
},
"schedule": {
"timeZone": "Europe/Warsaw",
"startDate": "2026-10-23T00:00:00Z",
"time": "2026-10-23T08:00:00Z",
"frequency": "Weekly"
}
}
}
}
},
"example": {
"recipients": {
"emails": [
]
}
}
}
Flags
| Flag | Type | Default | Example | Description |
|---|---|---|---|---|
--dry-run | boolean | false | If true, validates the request and returns the would-be result without applying it. The response then carries the X-Dry-Run header. |
Every command also accepts the CLI-wide flags for output shaping — see Output formats and Table output options.
Output
OK - Subscription updated, or previewed when dryRun=true. A patch that matches the stored values does not write and returns the current etag.
By default dci renders the result as a table. Use --output json to get the full structure described below — see Output formats.
| Field | Type | Description |
|---|---|---|
id | string | Server-assigned subscription id. null only on a dry-run create. |
name | string | Display name. For a scheduled subscription, \{source name\} plus (Dashboard Subscription), (Report Subscription), or (Commitment Subscription), from the nested type that sorts first. For an alert or a budget, the source name. |
createdBy | string (email) | Email address of the subscription creator. On create, the caller. |
recipients | object | Delivery targets shared by every nested subscription on this item. |
recipients.emails | array of string (email) | Email destinations in stored order. Reads return each address trimmed and lowercase. Addresses must belong to the organization's approved domains and must be a user or a pending invite. doit.com and doit-intl.com pass the domain check. A blank email, an email with no domain, or a domain the tenant does not allow is refused with 422 and code DOMAIN_NOT_APPROVED. An email that is not a user or a pending invite is refused with 422 and code NOT_INVITED_USER. The same email twice, ignoring case, is refused with 422 and code customers.duplicate_recipient. An address already stored, and still present, is not checked again. |
recipients.slackChannels | array of string | Slack channel ids in stored order. Reads return the public id. Ids are compared exactly after trim. A duplicate id is refused with 422 and code customers.duplicate_recipient. A blank or unconnected id is refused with 422 and code SLACK_CHANNEL_NOT_CONNECTED. This API does not list eligible channels. |
subscriptions | array of object | Nested subscriptions ordered by type ascending. The server sorts them. One entry per visible type. A scheduled subscription has one, two, or three scheduled types. An alert or a budget has exactly one entry. |
subscriptions[].type | string | Kind of nested subscription. |
subscriptions[].source | object | Source resource for this nested subscription. |
subscriptions[].source.id | string | ID of the dashboard, report, commitment, alert, or budget. Writable on create and on a patch of a scheduled subscription. On an alert or a budget it is the resource id. Report ids come from GET /analytics/v1/reports and GET /analytics/v1/reports/\{id\}. Commitment ids come from GET /analytics/v1/commitment-manager and GET /analytics/v1/commitment-manager/\{id\}. |
subscriptions[].source.name | string | Display name of the source resource. |
subscriptions[].schedule | object | Delivery schedule. Omitted for an alert or a budget. Never null. |
subscriptions[].schedule.timeZone | string | IANA time zone used to calculate deliveries. |
subscriptions[].schedule.startDate | string (date-time) | When this schedule starts. The start day is the calendar date of this instant in timeZone, not its UTC date. To start on 2026-11-03 in America/Los_Angeles, send 2026-11-03T00:00:00-08:00 (2026-11-03T08:00:00Z). A read returns the stored instant as UTC RFC3339 (Z). |
subscriptions[].schedule.time | string (date-time) | Stored instant of the delivery clock, returned as UTC RFC3339 (Z). An offset in the request is not preserved. 2026-10-23T10:00:00+02:00 is returned as 2026-10-23T08:00:00Z. The calendar date is the date of that instant. It is stored and returned, and it is not the subscription start date. Deliveries use only the hour and minute of that instant, interpreted in timeZone. Sending time already in Z makes the next read match the sent date and clock. |
subscriptions[].schedule.frequency | string | Console delivery frequency. |
subscriptions[].schedule.nextAt | string (date-time) | Next calculated delivery, returned as UTC RFC3339 (Z). The server sets it. A schedule change recalculates it. |
subscriptions[].schedule.customPeriodAmount | integer | Number of customPeriodUnit between deliveries. Present only when frequency is Custom. At least 1. |
subscriptions[].schedule.customPeriodUnit | string | Unit for customPeriodAmount. Present only when frequency is Custom. |
subscriptions[].attachments | array of string | Files to attach to the report subscription email. Present only for report-subscription. Always an array on read. Empty means no file. Rejected on any other type with 422. |
subscriptions[].pdfSettings | object | PDF rendering options. Present only for report-subscription. Always present on a report read. Rejected on any other type with 422. |
subscriptions[].pdfSettings.showFilters | boolean | Include the report filters summary in the PDF. |
subscriptions[].pdfSettings.includeTable | boolean | Include the data table in the PDF. |
subscriptions[].pdfSettings.includeAnnotations | boolean | Include annotations in the PDF time range. |
subscriptions[].permissions | object | Actions the authenticated caller may perform on this nested subscription. |
subscriptions[].permissions.canEditRecipients | boolean | Whether the caller may replace recipients. |
subscriptions[].permissions.canDelete | boolean | Whether the caller may delete this subscription. Always false for an alert or a budget. |
subscriptions[].permissions.canUnsubscribe | boolean | Whether the caller may remove their own recipient email. |
etag | string | Opaque version token for optimistic concurrency. 64 lowercase hex characters when set. The response ETag header is this value wrapped in double quotes. Send If-Match: "<etag>". null only on a dry-run create. It only covers fields this API can write. |
Example response (--output json)
{
"id": "notif_01HX9P2KQVJ8Z3M4T5N6V7W8Y9",
"name": "Executive costs (Dashboard Subscription)",
"recipients": {
"emails": [
],
"slackChannels": [
"C0123ABCD"
]
},
"subscriptions": [
{
"type": "dashboard-subscription",
"source": {
"id": "dashboard_123",
"name": "Executive costs"
},
"schedule": {
"frequency": "Weekly",
"time": "2026-10-23T08:00:00Z",
"timeZone": "Europe/Warsaw",
"startDate": "2026-10-23T00:00:00Z",
"nextAt": "2026-10-30T08:00:00Z"
},
"permissions": {
"canEditRecipients": true,
"canDelete": true,
"canUnsubscribe": false
}
}
],
"etag": "4a8c1e3b5d7f9a1c3e5b7d9f1a3c5e7a9b1d3f5a7c9e1b3d5f7a9c1e3b5d"
}
Raw JSON schema
{
"type": "object",
"additionalProperties": false,
"description": "One subscription. A scheduled subscription, an alert, and a budget use this same schema. `etag` is the opaque version token. Quote it when sending `If-Match` (`If-Match: \"<etag>\"`) on update and delete. The `ETag` response header carries that quoted form. `id` and `etag` are null only on a dry-run create.",
"required": [
"id",
"name",
"createdBy",
"recipients",
"subscriptions",
"etag"
],
"properties": {
"id": {
"type": "string",
"readOnly": true,
"x-terraform-computed": true,
"nullable": true,
"description": "Server-assigned subscription id. `null` only on a dry-run create.",
"example": "notif_01HX9P2KQVJ8Z3M4T5N6V7W8Y9"
},
"name": {
"type": "string",
"readOnly": true,
"x-terraform-computed": true,
"description": "Display name. For a scheduled subscription, `{source name}` plus `(Dashboard Subscription)`, `(Report Subscription)`, or `(Commitment Subscription)`, from the nested type that sorts first. For an alert or a budget, the source name.",
"example": "Executive costs (Dashboard Subscription)"
},
"createdBy": {
"type": "string",
"format": "email",
"readOnly": true,
"x-terraform-computed": true,
"x-doit-pii": true,
"description": "Email address of the subscription creator. On create, the caller.",
},
"recipients": {
"description": "Delivery targets shared by every nested subscription on this item.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Delivery targets. This object reads and writes email addresses and Slack channel ids only. Microsoft Teams and the in-app bell are not returned and cannot be set. A Teams destination stored in the Console still counts as a destination, so clearing email and Slack on that subscription does not return `409` `customers.last_destination`. Both arrays are always present on read. An unused list is an empty array, never omitted and never `null`. Emails are trimmed and stored lowercase. Channel ids are trimmed. A replaced list is stored in the order sent. Reads do not sort. A disconnected Slack channel stays in the list at its stored id. Create requires at least one email or Slack channel. A stored alert or budget may have both lists empty.",
"required": [
"emails",
"slackChannels"
],
"properties": {
"emails": {
"type": "array",
"minItems": 0,
"description": "Email destinations in stored order. Reads return each address trimmed and lowercase. Addresses must belong to the organization's approved domains and must be a user or a pending invite. `doit.com` and `doit-intl.com` pass the domain check. A blank email, an email with no domain, or a domain the tenant does not allow is refused with `422` and code `DOMAIN_NOT_APPROVED`. An email that is not a user or a pending invite is refused with `422` and code `NOT_INVITED_USER`. The same email twice, ignoring case, is refused with `422` and code `customers.duplicate_recipient`. An address already stored, and still present, is not checked again.",
"items": {
"type": "string",
"format": "email",
"minLength": 1,
"x-doit-pii": true
},
"example": [
]
},
"slackChannels": {
"type": "array",
"minItems": 0,
"description": "Slack channel ids in stored order. Reads return the public id. Ids are compared exactly after trim. A duplicate id is refused with `422` and code `customers.duplicate_recipient`. A blank or unconnected id is refused with `422` and code `SLACK_CHANNEL_NOT_CONNECTED`. This API does not list eligible channels.",
"items": {
"type": "string",
"minLength": 1
},
"example": [
"C0123ABCD"
]
}
},
"example": {
"emails": [
],
"slackChannels": [
"C0123ABCD"
]
}
}
]
},
"subscriptions": {
"type": "array",
"format": "set",
"minItems": 1,
"description": "Nested subscriptions ordered by `type` ascending. The server sorts them. One entry per visible type. A scheduled subscription has one, two, or three scheduled types. An alert or a budget has exactly one entry.",
"items": {
"type": "object",
"additionalProperties": false,
"description": "One nested subscription. `schedule` is present for a dashboard, report, or commitment subscription and omitted for an alert or a budget. `attachments` and `pdfSettings` are present only for a `report-subscription` and omitted for every other type.",
"required": [
"type",
"source",
"permissions"
],
"properties": {
"type": {
"description": "Kind of nested subscription.",
"allOf": [
{
"type": "string",
"description": "Kind of subscription nested on one item. One id namespace is shared by a scheduled subscription, an alert, and a budget. Create accepts only `dashboard-subscription`, `report-subscription`, and `commitment-manager`. GET returns `alert` and `budget`. PATCH may replace their recipient lists, and that is the one write this API makes on them. Those lists are the same ones written by `PATCH /analytics/v1/alerts/{id}` and `PATCH /analytics/v1/budgets/{id}`. DELETE of an alert or a budget returns `409` `customers.entry_is_resource`. `EntryPatch.type` includes `alert` and `budget` so an echo of the stored entry validates.",
"example": "dashboard-subscription",
"enum": [
"alert",
"budget",
"commitment-manager",
"dashboard-subscription",
"report-subscription"
],
"x-enumDescriptions": {
"alert": "Alert entry. One item, and it cannot be deleted through this API.",
"budget": "Budget entry. One item, and it cannot be deleted through this API.",
"commitment-manager": "Commitment subscription. Scheduled delivery of a commitment in Commitment manager.",
"dashboard-subscription": "Dashboard subscription. Scheduled email or Slack delivery of a dashboard.",
"report-subscription": "Report subscription. Scheduled email or Slack delivery of a report."
}
}
],
"example": "dashboard-subscription"
},
"source": {
"description": "Source resource for this nested subscription.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Source resource for one nested subscription. `name` is read live from the source and can change without a write to this API.",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"description": "ID of the dashboard, report, commitment, alert, or budget. Writable on create and on a patch of a scheduled subscription. On an alert or a budget it is the resource id. Report ids come from `GET /analytics/v1/reports` and `GET /analytics/v1/reports/{id}`. Commitment ids come from `GET /analytics/v1/commitment-manager` and `GET /analytics/v1/commitment-manager/{id}`.",
"example": "dashboard_123"
},
"name": {
"type": "string",
"readOnly": true,
"x-terraform-computed": true,
"description": "Display name of the source resource.",
"example": "Executive costs"
}
},
"example": {
"id": "dashboard_123",
"name": "Executive costs"
}
}
]
},
"schedule": {
"description": "Delivery schedule. Omitted for an alert or a budget. Never `null`.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Delivery schedule for a dashboard, report, or commitment subscription. Omitted on an alert or a budget, never `null`. `customPeriodAmount` and `customPeriodUnit` are omitted unless `frequency` is `Custom`. `nextAt` moves when the next delivery is calculated.",
"required": [
"timeZone",
"startDate",
"time",
"frequency",
"nextAt"
],
"properties": {
"timeZone": {
"type": "string",
"minLength": 1,
"description": "IANA time zone used to calculate deliveries.",
"example": "Europe/Warsaw"
},
"startDate": {
"type": "string",
"format": "date-time",
"description": "When this schedule starts. The start day is the calendar date of this instant in `timeZone`, not its UTC date. To start on 2026-11-03 in `America/Los_Angeles`, send `2026-11-03T00:00:00-08:00` (`2026-11-03T08:00:00Z`). A read returns the stored instant as UTC RFC3339 (`Z`).",
"example": "2026-11-03T08:00:00Z"
},
"time": {
"type": "string",
"format": "date-time",
"description": "Stored instant of the delivery clock, returned as UTC RFC3339 (`Z`). An offset in the request is not preserved. `2026-10-23T10:00:00+02:00` is returned as `2026-10-23T08:00:00Z`. The calendar date is the date of that instant. It is stored and returned, and it is not the subscription start date. Deliveries use only the hour and minute of that instant, interpreted in `timeZone`. Sending `time` already in `Z` makes the next read match the sent date and clock.",
"example": "2026-10-23T08:00:00Z"
},
"frequency": {
"description": "Console delivery frequency.",
"allOf": [
{
"type": "string",
"description": "Console delivery frequency. Wire values match the Console labels.",
"example": "Weekly",
"enum": [
"Daily",
"Weekly",
"Monthly",
"Annually",
"Custom"
],
"x-enumDescriptions": {
"Daily": "Deliver every day at the configured time.",
"Weekly": "Deliver once a week at the configured time.",
"Monthly": "Deliver once a month at the configured time.",
"Annually": "Deliver once a year at the configured time.",
"Custom": "Deliver on the interval in `customPeriodAmount` and `customPeriodUnit`."
}
}
],
"example": "Weekly"
},
"nextAt": {
"type": "string",
"format": "date-time",
"readOnly": true,
"x-terraform-computed": true,
"description": "Next calculated delivery, returned as UTC RFC3339 (`Z`). The server sets it. A schedule change recalculates it.",
"example": "2026-10-30T08:00:00Z"
},
"customPeriodAmount": {
"type": "integer",
"minimum": 1,
"description": "Number of `customPeriodUnit` between deliveries. Present only when `frequency` is `Custom`. At least 1.",
"example": 2
},
"customPeriodUnit": {
"description": "Unit for `customPeriodAmount`. Present only when `frequency` is `Custom`.",
"allOf": [
{
"type": "string",
"description": "Unit for a `Custom` frequency.",
"example": "Month",
"enum": [
"Day",
"Week",
"Month",
"Year"
],
"x-enumDescriptions": {
"Day": "The custom interval is a number of days.",
"Week": "The custom interval is a number of weeks.",
"Month": "The custom interval is a number of months.",
"Year": "The custom interval is a number of years."
}
}
],
"example": "Month"
}
},
"example": {
"timeZone": "Europe/Warsaw",
"startDate": "2026-10-23T00:00:00Z",
"time": "2026-10-23T08:00:00Z",
"frequency": "Weekly",
"nextAt": "2026-10-30T08:00:00Z"
}
}
]
},
"attachments": {
"type": "array",
"uniqueItems": true,
"description": "Files to attach to the report subscription email. Present only for `report-subscription`. Always an array on read. Empty means no file. Rejected on any other type with `422`.",
"items": {
"type": "string",
"description": "File to attach to a report subscription email. Values may appear at most once. Attachments are email-only: Slack-only subscriptions may still store the choice, and delivery skips the files when there is no email recipient. An oversized file is omitted from that email with a note in the body. This API does not upload or return file bytes.",
"example": "pdf",
"enum": [
"pdf",
"csv"
],
"x-enumDescriptions": {
"pdf": "Attach a PDF of the report to the email.",
"csv": "Attach a CSV export of the report to the email."
}
},
"example": [
"pdf",
"csv"
]
},
"pdfSettings": {
"description": "PDF rendering options. Present only for `report-subscription`. Always present on a report read. Rejected on any other type with `422`.",
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "PDF rendering options for a report subscription. Present on every `report-subscription` read. A missing stored object reads as all `true`. The flags apply only when `pdf` is in `attachments`. They may still be stored while PDF is off.",
"required": [
"showFilters",
"includeTable",
"includeAnnotations"
],
"properties": {
"showFilters": {
"type": "boolean",
"description": "Include the report filters summary in the PDF.",
"example": true
},
"includeTable": {
"type": "boolean",
"description": "Include the data table in the PDF.",
"example": true
},
"includeAnnotations": {
"type": "boolean",
"description": "Include annotations in the PDF time range.",
"example": true
}
},
"example": {
"showFilters": true,
"includeTable": true,
"includeAnnotations": true
}
}
]
},
"permissions": {
"description": "Actions the authenticated caller may perform on this nested subscription.",
"readOnly": true,
"x-terraform-computed": true,
"allOf": [
{
"type": "object",
"additionalProperties": false,
"description": "Actions the authenticated caller may perform on this nested subscription. Computed per request; a different token can see different values.",
"readOnly": true,
"x-terraform-computed": true,
"required": [
"canEditRecipients",
"canDelete",
"canUnsubscribe"
],
"properties": {
"canEditRecipients": {
"type": "boolean",
"description": "Whether the caller may replace recipients.",
"example": true
},
"canDelete": {
"type": "boolean",
"description": "Whether the caller may delete this subscription. Always `false` for an alert or a budget.",
"example": true
},
"canUnsubscribe": {
"type": "boolean",
"description": "Whether the caller may remove their own recipient email.",
"example": false
}
},
"example": {
"canEditRecipients": true,
"canDelete": true,
"canUnsubscribe": false
}
}
]
}
},
"example": {
"type": "dashboard-subscription",
"source": {
"id": "dashboard_123",
"name": "Executive costs"
},
"schedule": {
"timeZone": "Europe/Warsaw",
"startDate": "2026-10-23T00:00:00Z",
"time": "2026-10-23T08:00:00Z",
"frequency": "Weekly",
"nextAt": "2026-10-30T08:00:00Z"
},
"permissions": {
"canEditRecipients": true,
"canDelete": true,
"canUnsubscribe": false
}
}
}
},
"etag": {
"type": "string",
"readOnly": true,
"x-terraform-computed": true,
"nullable": true,
"description": "Opaque version token for optimistic concurrency. 64 lowercase hex characters when set. The response `ETag` header is this value wrapped in double quotes. Send `If-Match: \"<etag>\"`. `null` only on a dry-run create. It only covers fields this API can write.",
"example": "3f7a1c9e2b4d6f8a0c1e3a5b7d9f1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e"
}
},
"example": {
"id": "notif_01HX9P2KQVJ8Z3M4T5N6V7W8Y9",
"name": "Executive costs (Dashboard Subscription)",
"recipients": {
"emails": [
],
"slackChannels": [
"C0123ABCD"
]
},
"subscriptions": [
{
"type": "dashboard-subscription",
"source": {
"id": "dashboard_123",
"name": "Executive costs"
},
"schedule": {
"timeZone": "Europe/Warsaw",
"startDate": "2026-10-23T00:00:00Z",
"time": "2026-10-23T08:00:00Z",
"frequency": "Weekly",
"nextAt": "2026-10-30T08:00:00Z"
},
"permissions": {
"canEditRecipients": true,
"canDelete": true,
"canUnsubscribe": false
}
}
],
"etag": "3f7a1c9e2b4d6f8a0c1e3a5b7d9f1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e"
}
}
Errors
On failure, dci prints a single error message — with a hint when one is available — and exits with a typed code your scripts can branch on. See Errors and exit codes for the full contract.
HTTP status to exit code mapping
| HTTP status | Exit code | Error code | Meaning |
|---|---|---|---|
| 400, 422 | 30 | VALIDATION_ERROR | The arguments or request body were rejected. Review the command's flags and payload. |
| 401 | 10 | AUTHENTICATION_FAILED | Not signed in, or the API token is invalid. Run dci login or check DCI_API_KEY. |
| 403 | 11 | PERMISSION_DENIED | The DoiT user or the active customer context does not have access. |
| 404 | 20 | RESOURCE_NOT_FOUND | The requested resource does not exist. Check the identifier argument. |
| 409 | 21 | RESOURCE_CONFLICT | The operation conflicts with the resource's current state. |
| 412, 415, 428 | 1 | API_ERROR | The request failed; the printed error message has details. |
| 500 | 40 | API_SERVER_ERROR | The API failed to process the request. Retryable; contact DoiT support if it persists. |
Related
- create-customer-subscription — Create a subscription
- delete-customer-subscription — Delete a subscription
- get-customer-subscription — Retrieve a subscription
- list-customer-subscriptions — List subscriptions
- API reference: PATCH /customers/v1/subscriptions/{id}
Aliases: updatecustomersubscription