Skip to content

Call orders

A call order is one instruction: call this person, about this claim, for this purpose. It is the whole of Model A, and it is also what Model B produces internally - a sequence’s call step creates an ordinary call order rather than dialling by itself, so there is only ever one dial path and one result derivation.

An order that goes well walks a straight line. The only loop is the retry:

stateDiagram-v2
    direction LR
    [*] --> accepted
    accepted --> scheduled
    scheduled --> in_progress
    in_progress --> completed
    in_progress --> scheduled: retry
    completed --> [*]

Three further states - canceled, expired and failed - end an order early and are reachable from more than one point on that line. They are listed in full under leaving the path early rather than drawn, because every one of them is an edge from three different places and the picture stops being readable long before it stops being complete.

Four of the seven states are terminal: completed, canceled, expired and failed. This is a closed enum in the schema, not an open registry - resource states are a contract, and Pace adds nothing to this list within v1. You can build a state machine on it.

State Meaning
accepted Validated and stored. Nobody has been called.
scheduled A permissible dial instant has been computed; next_attempt_at is set.
in_progress The planner has claimed the order and is dialling.
completed The order is finished. A result is present.
canceled You cancelled it, or a suppression arrived.
expired The window passed before a permissible attempt could be made.
failed A platform error. Not a business outcome.

Every transition off the straight line, and what causes it.

From To Trigger
accepted expired The window’s not_after passed before any dial.
accepted canceled You called cancel.
accepted failed An unrecoverable platform or validation error after acceptance.
scheduled expired not_after passed with attempts still remaining.
scheduled canceled You called cancel, before any attempt was claimed.
scheduled failed An unrecoverable platform error while scheduling.
in_progress canceled A suppression for this subject arrived mid-flight.
in_progress failed An unrecoverable platform error during the attempt.

The two in_progress rows are the ones worth noting. A cancellation you request is refused once dialling has started - but a suppression still lands, because the question it answers is not “do you still want this call?” but “is this person allowed to be called?”.

A 202 is not a promise that a call will happen. It means the order got past:

  • the order type exists, is active, and its version is pinned onto the order;
  • every field your tenant’s verification mode requires is present on subject;
  • context.variables validates against the order type’s variables_schema;
  • the negotiation corridor you sent is no wider than the order type’s;
  • the subject is not on the suppression list;
  • at least one contact has channel: "phone";
  • your window intersects the permitted window somewhere in the next 30 days.

Anything that fails here comes back as a 422 with the offending field in errors[].pointer. See Handling errors.

You may send a window and local calling times. Both are treated as a narrowing:

actual = your window
∩ statutory calling hours
∩ your tenant's configured hours
∩ the per-subject daily cap
∩ available capacity

If that intersection is empty, on_window_violation decides what happens. defer (the default) moves the order into the permitted window, where it may eventually expire. reject refuses acceptance with 422 on_window_violation, which is the right choice when a call outside your window is worse to you than no call at all.

Dial instants are computed on a fifteen-minute grid, so the first attempt lands at the next quarter-hour at the earliest. The search looks 30 days ahead; a window with no permissible instant inside that horizon is treated as unsatisfiable and rejected at acceptance with window_unsatisfiable, rather than accepted and quietly expired a month later.

Retries are governed by scheduling.retry:

{
"scheduling": {
"max_attempts": 3,
"retry": {
"min_gap_hours": 24,
"randomize_within_window": true,
"retry_on": ["no_answer", "busy", "voicemail"]
}
}
}

A conversation is never retried. Not even when the outcome was not the one you wanted. Only attempts that failed to reach a live counterpart can trigger another one - the rule is in the platform, not in your configuration, and retry_on can only narrow it further.

randomize_within_window scatters the redial instant so the same person is not called at the same minute every day. Leave it on unless you have a specific reason; a predictable daily call at 09:03 reads as harassment even when the count is lawful.

PATCH /v1/call-orders/{order_id} accepts exactly two fields: scheduling and metadata. It works only before the first dial attempt.

Everything that bears on the conversation is immutable after acceptance. A claim amount that changed between acceptance and the call would leave a result nobody could explain afterwards - you would not know which figure the agent actually spoke. If the claim changed, cancel and submit a new order.

The endpoint supports ETag and If-Match. Read the order, keep the ETag, send it back on the PATCH:

Terminal window
ETAG=$(curl -sS -D - -o /dev/null "$BASE/v1/call-orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN" | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')
curl -sS -X PATCH "$BASE/v1/call-orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "If-Match: $ETAG" \
-H "Content-Type: application/json" \
-d '{"scheduling": {"max_attempts": 2}}'

A stale ETag returns 412 precondition_failed. The check is atomic against the stored revision, so two concurrent edits cannot both win.

POST /v1/call-orders/{order_id}/cancel works in accepted and scheduled. In in_progress it returns 409 order_not_cancelable - by then the telephone is already ringing, and there is no honest way to unring it.

Between “instant computed” and “it rings” the planner deliberately leaves one tick, so a cancellation or a suppression arriving in that gap still takes effect.

An order can also be cancelled without you asking: if a suppression for that subject arrives mid-flight, the order is cancelled with cancel_reason: "suppressed".

GET /v1/call-orders filters on seven fields, including client_reference, enrollment_id, status and order_type. client_reference is your own business key - it is mirrored onto the resource and into every event, and it does not have to be unique, so it is usually the right thing to filter on when reconciling against your own system.

Lists use the standard envelope:

{
"data": [ ],
"has_more": true,
"next_cursor": "eyJ0IjoxNzI…"
}

has_more is authoritative. Never infer the end of a list from the length of data

  • a page can be short without being last. next_cursor is opaque: pass it back as after, and never parse, construct or compare it.