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.
States
Section titled “States”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. |
Leaving the path early
Section titled “Leaving the path early”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?”.
What acceptance actually checks
Section titled “What acceptance actually checks”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.variablesvalidates against the order type’svariables_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.
Scheduling and the window intersection
Section titled “Scheduling and the window intersection”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 capacityIf 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
Section titled “Retries”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.
Modifying an order
Section titled “Modifying an order”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:
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.
Cancelling
Section titled “Cancelling”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".
Reading orders back
Section titled “Reading orders back”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_cursoris opaque: pass it back asafter, and never parse, construct or compare it.