Skip to content

Disposition rules ​

Generated from DialerDigital/core commit 0fc18008b3c8 — spec openapi/dialer-v1.yaml v0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Español

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 ​

MethodPathSummary
GET/v1/disposition-rulesList the tenant's retry policy by outcome
PUT/v1/disposition-rulesReplace the tenant's reschedule/suppress policy
POST/v1/disposition-rules/seed-defaultsInstall the canonical retry cadences
GET/v1/campaigns/{id}/disposition-rulesRead one campaign's retry policy
PUT/v1/campaigns/{id}/disposition-rulesReplace one campaign's reschedule/suppress policy

List the tenant's retry policy by outcome ​

http
GET /v1/disposition-rules

Every 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.

FieldTypeRequiredDescription
disposition_rulesarray of DispositionRuleyes
disposition_rules[].idstring (uuid)yes
disposition_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
disposition_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
disposition_rules[].priorityintegeryesLower runs first.
disposition_rules[].activebooleanyes
disposition_rules[].updated_atstring (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-rules

PUT 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)

FieldTypeRequiredDescription
rulesarray of objectyes
rules[].codestringyes
rules[].effectreschedule · suppressyes
rules[].priorityintegerno
rules[].activebooleanno
rules[].paramsobjectnoRequired for reschedule (delay_seconds and max_attempts required inside). Optional for suppress (only reason).
rules[].params.delay_secondsintegerno
rules[].params.factornumberno
rules[].params.max_delay_secondsintegerno
rules[].params.max_attemptsintegernoHow 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.reasonstringno
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).

FieldTypeRequiredDescription
disposition_rulesarray of DispositionRuleyes
disposition_rules[].idstring (uuid)yes
disposition_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
disposition_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
disposition_rules[].priorityintegeryesLower runs first.
disposition_rules[].activebooleanyes
disposition_rules[].updated_atstring (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-defaults

Writes 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).

FieldTypeRequiredDescription
disposition_rulesarray of DispositionRuleyes
disposition_rules[].idstring (uuid)yes
disposition_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
disposition_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
disposition_rules[].priorityintegeryesLower runs first.
disposition_rules[].activebooleanyes
disposition_rules[].updated_atstring (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-rules

disposition_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.

FieldTypeRequiredDescription
campaign_idstring (uuid)yes
disposition_rulesarray of DispositionRuleyes
disposition_rules[].idstring (uuid)yes
disposition_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
disposition_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
disposition_rules[].priorityintegeryesLower runs first.
disposition_rules[].activebooleanyes
disposition_rules[].updated_atstring (date-time)yes
effective_rulesarray of DispositionRuleyes
effective_rules[].idstring (uuid)yes
effective_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
effective_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
effective_rules[].priorityintegeryesLower runs first.
effective_rules[].activebooleanyes
effective_rules[].updated_atstring (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-rules

Same 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)

FieldTypeRequiredDescription
rulesarray of objectyes
rules[].codestringyes
rules[].effectreschedule · suppressyes
rules[].priorityintegerno
rules[].activebooleanno
rules[].paramsobjectnoRequired for reschedule (delay_seconds and max_attempts required inside). Optional for suppress (only reason).
rules[].params.delay_secondsintegerno
rules[].params.factornumberno
rules[].params.max_delay_secondsintegerno
rules[].params.max_attemptsintegernoHow 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.reasonstringno
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.

FieldTypeRequiredDescription
campaign_idstring (uuid)yes
disposition_rulesarray of DispositionRuleyes
disposition_rules[].idstring (uuid)yes
disposition_rules[].campaign_idstring,null (uuid)nonull 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[].codestringyesThe call outcome the rule reacts to, matched verbatim (busy, no_answer, ...).
disposition_rules[].effectstringyesreschedule 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[].paramsobjectyesFor reschedule: delay_seconds, factor, max_delay_seconds, max_attempts (all four stored). For suppress: optional reason.
disposition_rules[].priorityintegeryesLower runs first.
disposition_rules[].activebooleanyes
disposition_rules[].updated_atstring (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"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.