Appearance
Disposition rules
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
The tenant's retry policy by call outcome (R10): reschedule or suppress a debt after a given disposition. It can only postpone or close a debt; every dial still runs the compliance gates at originate time.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/disposition-rules | List the tenant's retry policy by outcome |
PUT | /v1/disposition-rules | Replace the tenant's reschedule/suppress policy |
POST | /v1/disposition-rules/seed-defaults | Install the canonical retry cadences |
GET | /v1/campaigns/{id}/disposition-rules | Read one campaign's retry policy |
PUT | /v1/campaigns/{id}/disposition-rules | Replace one campaign's reschedule/suppress policy |
List the tenant's retry policy by outcome
http
GET /v1/disposition-rulesEvery TENANT-scope rule — active or not, any effect — ordered by code, then priority. A switched-off rule is still listed (active: false): rules are never deleted, so the history of a cadence stays readable. Every rule listed here has campaign_id: null; a campaign's own rules are read at GET /v1/campaigns/{id}/disposition-rules, and for the codes a campaign has active rules for, those win over these in that campaign.
A tenant with NO active rule has an inert policy: closing an attempt changes nothing about when its debt is dialable again. The canonical cadences are installed only on request (POST /v1/disposition-rules/seed-defaults).
max_attempts bounds how many times THIS policy reschedules a debt (the count is every original attempt of the debt, any outcome). It is not a regulatory ceiling and does not move one: the compliance gates re-run at originate time on every dial, whatever the policy says.
Responses
200
The tenant's rules.
| Field | Type | Required | Description |
|---|---|---|---|
disposition_rules | array of DispositionRule | yes | |
disposition_rules[].id | string (uuid) | yes | |
disposition_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
disposition_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
disposition_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
disposition_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
disposition_rules[].priority | integer | yes | Lower runs first. |
disposition_rules[].active | boolean | yes | |
disposition_rules[].updated_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Replace the tenant's reschedule/suppress policy
http
PUT /v1/disposition-rulesPUT semantics over the two writable effects (reschedule, suppress), in the TENANT scope: every listed (code, effect) is written as given, and every ACTIVE tenant-scope reschedule/suppress rule that is NOT listed is switched off (active: false, never deleted). An empty rules array turns the tenant's policy off. Rules with any other effect, and every campaign's own rules, are neither written nor retired by this call.
Validation is strict and reports EVERY problem in one 422, each one naming the element and the field (rules[1].params.max_attempts must be an integer between 1 and 100): code non-empty (at most 64 characters, no surrounding whitespace, no null byte); delay_seconds and max_delay_seconds JSON integers between 0 and 2592000, with max_delay_seconds >= delay_seconds; factor a number between 1 and 10; max_attempts a JSON integer between 1 and 100; priority a JSON integer between 0 and 2147483647; reason without a null byte; at most one rule per (code, effect); at most 100 rules; no unknown field at any level. The two ceilings on factor and max_attempts together keep the growth factor^(attempts - 1) finite. factor (default 2.0) and max_delay_seconds (default 86400) may be omitted: the value the engine applies is then STORED, so a read shows what is applied. An omitted priority is 100 and an omitted active is true.
The delay grows as delay_seconds * factor^(attempts - 1), capped at max_delay_seconds; once max_attempts is reached the debt is closed instead of rescheduled. The next dialable instant is computed from the closed attempt's end time, not the wall clock, so re-processing the same attempt yields the same instant.
Every change is recorded in the tenant's append-only config audit trail (who, from what, to what) in the same transaction as the write; an unchanged rule writes nothing. Tenant-management scope (dd_ key or dashboard login; ddw_ → 403).
Role rules (core#2438): a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob compliance_config_admin_only, default on). The GET of this resource is not affected.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
rules | array of object | yes | |
rules[].code | string | yes | |
rules[].effect | reschedule · suppress | yes | |
rules[].priority | integer | no | |
rules[].active | boolean | no | |
rules[].params | object | no | Required for reschedule (delay_seconds and max_attempts required inside). Optional for suppress (only reason). |
rules[].params.delay_seconds | integer | no | |
rules[].params.factor | number | no | |
rules[].params.max_delay_seconds | integer | no | |
rules[].params.max_attempts | integer | no | How many original attempts of the debt (any outcome) this policy allows before closing it instead of rescheduling. Not a regulatory ceiling; the compliance gates decide independently on every dial. |
rules[].params.reason | string | no |
json
{
"rules": [
{
"code": "busy",
"effect": "reschedule",
"priority": 10,
"params": {
"delay_seconds": 1200,
"factor": 2,
"max_delay_seconds": 86400,
"max_attempts": 6
}
},
{
"code": "invalid_number",
"effect": "suppress",
"params": {
"reason": "invalid_number"
}
}
]
}Responses
200
Every rule of the tenant after the write (same shape as the GET).
| Field | Type | Required | Description |
|---|---|---|---|
disposition_rules | array of DispositionRule | yes | |
disposition_rules[].id | string (uuid) | yes | |
disposition_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
disposition_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
disposition_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
disposition_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
disposition_rules[].priority | integer | yes | Lower runs first. |
disposition_rules[].active | boolean | yes | |
disposition_rules[].updated_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Install the canonical retry cadences
http
POST /v1/disposition-rules/seed-defaultsWrites the canonical rules: busy → reschedule (1200 s, factor 2.0, 6 attempts), no_answer → reschedule (7200 s, factor 1.5, 5 attempts), answered_machine → reschedule (86400 s, factor 1.0, 3 attempts), and invalid_number → suppress; every reschedule caps at 86400 s. A canonical code the tenant had customised is restored to these values; every other rule is left alone. Idempotent: a second call changes nothing and records nothing. Explicit on purpose — installing a policy changes what the tenant dials, so no deploy or migration ever does it. Audited like the PUT. Takes no body (any field is a 400). Tenant-management scope (dd_ key or dashboard login; ddw_ → 403).
Role rules (core#2438): a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob compliance_config_admin_only, default on). The GET of this resource is not affected.
Responses
200
Every rule of the tenant after seeding (same shape as the GET).
| Field | Type | Required | Description |
|---|---|---|---|
disposition_rules | array of DispositionRule | yes | |
disposition_rules[].id | string (uuid) | yes | |
disposition_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
disposition_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
disposition_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
disposition_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
disposition_rules[].priority | integer | yes | Lower runs first. |
disposition_rules[].active | boolean | yes | |
disposition_rules[].updated_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Read one campaign's retry policy
http
GET /v1/campaigns/{id}/disposition-rulesdisposition_rules are the campaign's OWN rules, active or not (same order and shape as GET /v1/disposition-rules, with campaign_id set).
effective_rules are the ACTIVE rules that apply to an attempt of this campaign. The precedence is per code: when the campaign has at least one active rule for a code, the campaign's rules for that code are the policy for it in this campaign and the tenant's rules for that code are not applied here; for every other code the tenant's rules apply. Each entry's campaign_id says which scope it came from (null = the tenant's). This list is computed by the same precedence the dialer applies when an attempt closes, so it is what is applied. The campaign that decides is the campaign of the ATTEMPT.
A campaign with no rules of its own dials exactly under the tenant's policy. A campaign that is not the tenant's (or a malformed id) is a 404.
Responses
200
The campaign's own rules and the rules that apply to it.
| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string (uuid) | yes | |
disposition_rules | array of DispositionRule | yes | |
disposition_rules[].id | string (uuid) | yes | |
disposition_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
disposition_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
disposition_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
disposition_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
disposition_rules[].priority | integer | yes | Lower runs first. |
disposition_rules[].active | boolean | yes | |
disposition_rules[].updated_at | string (date-time) | yes | |
effective_rules | array of DispositionRule | yes | |
effective_rules[].id | string (uuid) | yes | |
effective_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
effective_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
effective_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
effective_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
effective_rules[].priority | integer | yes | Lower runs first. |
effective_rules[].active | boolean | yes | |
effective_rules[].updated_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Replace one campaign's reschedule/suppress policy
http
PUT /v1/campaigns/{id}/disposition-rulesSame body, validation and PUT semantics as PUT /v1/disposition-rules, over this campaign's own rules only: every listed (code, effect) is written for the campaign, and every ACTIVE campaign reschedule/suppress rule that is NOT listed is switched off (active: false, never deleted). The tenant's rules and every other campaign's are never written nor retired by this call. An empty rules array switches the campaign's own rules off, which returns the campaign to the tenant's policy.
A campaign rule can only postpone (reschedule) or close (suppress) a debt, like a tenant rule; the compliance gates re-run at originate time on every dial. Audited in the tenant's config trail in the same transaction, under the key campaign_disposition_rule:<campaign_id>:<code>:<effect>. Tenant-management scope (dd_ key or dashboard login; ddw_ → 403). A campaign that is not the tenant's is a 404 and nothing is written.
Role rules (core#2438): a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob compliance_config_admin_only, default on). The GET of this resource is not affected.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
rules | array of object | yes | |
rules[].code | string | yes | |
rules[].effect | reschedule · suppress | yes | |
rules[].priority | integer | no | |
rules[].active | boolean | no | |
rules[].params | object | no | Required for reschedule (delay_seconds and max_attempts required inside). Optional for suppress (only reason). |
rules[].params.delay_seconds | integer | no | |
rules[].params.factor | number | no | |
rules[].params.max_delay_seconds | integer | no | |
rules[].params.max_attempts | integer | no | How many original attempts of the debt (any outcome) this policy allows before closing it instead of rescheduling. Not a regulatory ceiling; the compliance gates decide independently on every dial. |
rules[].params.reason | string | no |
json
{
"rules": [
{
"code": "no_answer",
"effect": "reschedule",
"priority": 10,
"params": {
"delay_seconds": 14400,
"factor": 1,
"max_delay_seconds": 14400,
"max_attempts": 3
}
}
]
}Responses
200
The campaign's own rules after the write.
| Field | Type | Required | Description |
|---|---|---|---|
campaign_id | string (uuid) | yes | |
disposition_rules | array of DispositionRule | yes | |
disposition_rules[].id | string (uuid) | yes | |
disposition_rules[].campaign_id | string,null (uuid) | no | null for a tenant rule; the campaign's id for a rule of that campaign, which wins over the tenant's rules for its code in that campaign. |
disposition_rules[].code | string | yes | The call outcome the rule reacts to, matched verbatim (busy, no_answer, ...). |
disposition_rules[].effect | string | yes | reschedule or suppress (the two this API writes). A rule stored with another effect is listed as it is and never written by this API. |
disposition_rules[].params | object | yes | For reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason. |
disposition_rules[].priority | integer | yes | Lower runs first. |
disposition_rules[].active | boolean | yes | |
disposition_rules[].updated_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}