SKIP_INVALID_ROWS (non-strict mode) is the new mode. STRICT_VALIDATION is the default behavior that call jobs API always had, and remains here for backwards compatibility when validation_mode is not passed.
Either use the call job API in STRICT_VALIDATION to require every row to pass, or SKIP_INVALID_ROWS to schedule the rows that pass and return errors for the rest. Both modes validate row schemas and business rules before creating scheduled requests. The difference is whether one invalid row rejects the entire batch.
Set validation_mode in the JSON request body for POST /scheduler/call-jobs. It defaults to STRICT_VALIDATION when omitted; the Call Jobs API documents the full request fields.
Behavior
STRICT_VALIDATION
SKIP_INVALID_ROWS
Some rows fail
Reject the entire batch with HTTP 422
Return HTTP 200; schedule valid rows and report invalid rows
All rows fail
Reject the entire batch with HTTP 422
Return HTTP 200 with all rows INVALID and call_job_id: null
Row-error location
detail for schema errors; detail.errors for business validation errors
scheduled_requests, with errors on each INVALID row
Validation stages used by both modes
Validation is split into two stages, and business validation only runs on rows whose schema parsed. A row reports business validation errors only when its schema is valid.
Schema validation parses each row into the call schema. It catches a missing or malformed required field, a wrong type, an invalid objective, a bad date, time, or phone number format, and unknown fields.
Business-rule validation runs only after a row parses. It catches problems the schema cannot see: an organization_name that is not configured for your tenant, a provider_zip that does not resolve to a timezone, expected_appointment_date_time that is not in the past for CONFIRM_IE_ATTENDANCE, custom call-input contract failures defined by our agreements with clients, and call-window rules such as an unsupported timezone or no remaining call window.
Business checks run in sequence and stop at the first stage that fails, so a row reports only the errors from the checks that actually ran.
Dry run in either mode: validate without persisting
Pass validate_only_without_persistence=true as a query parameter in the request URL to run both stages over every row in either mode with no side effects. Do not include this flag in the JSON request body.
POST /scheduler/call-jobs?validate_only_without_persistence=true
Row-level failures return HTTP 200 even when every row fails, so a strict dry run surfaces business validation errors that a real strict submission would hide behind its first schema failure. The response is a validation report:
Rows that pass carry statusVALID with the resolved resolved_timezone; failing rows are INVALID with the same field and message errors a real submission would return. Invalid batch structure is still rejected with 422.
For either submission mode, ENQUEUED means accepted for scheduling, not finished. A workflow-start failure can still occur after persistence and is reported in the accepted row's singular error field. For a non-null call_job_id, see Track specific calls after a batch API call.
Strict mode: STRICT_VALIDATION (default)
Use strict mode when the entire submission must pass before any row is scheduled. Set validation_mode: "STRICT_VALIDATION" or omit the field; any row error rejects the batch with HTTP 422, with no job, scheduled requests, or workflow created.
The batch advances to business validation only after every row passes schema validation.
Schema validation parses every row. If any row has a schema error, the request fails with HTTP 422 and the raw detail list described below. Business validation never runs, so business problems in later rows are never reported.
If every row parses, business-rule validation runs on every row. If any row fails a business rule, the request fails with HTTP 422 and the detail.errors object described below.
Nothing is persisted and no workflow starts unless every row passes both stages.
When every row passes, the normal submission response is HTTP 200 with call_job_id, submitted_at, and the accepted rows in scheduled_requests. A non-null call_job_id identifies the created job; accepted rows normally have status: "ENQUEUED".
Schema errors: HTTP 422 with a detail list
Strict-mode row schema errors return a validation error list under detail. Batch-level errors use this same shape in either mode. Each entry carries type, loc, msg, and input; row errors whose location starts with ["body", "calls", row_index] use a zero-based row index.
The simplified loc above points at row 3 (index 2) and its provider_zip field. Row locations may also include the selected objective before the field, for example ["body", "calls", 2, "CONFIRM_IE_ATTENDANCE", "provider_zip"]; that objective is a schema label, not a nested input field.
The batch-wide external_reference_id error uses loc: ["body"]. Check whether a location includes a row index before reading it:
for err in body["detail"]: loc = err["loc"] if len(loc) >= 3 and loc[:2] == ["body", "calls"] and isinstance(loc[2], int): # Keep the full location, including any objective or nested field path. print("row", loc[2], loc[3:], err["msg"]) else: print("batch", loc, err["msg"])
for (const err of body.detail) { const loc = err.loc; if (loc[0] === "body" && loc[1] === "calls" && Number.isInteger(loc[2])) { // Keep the full location, including any objective or nested field path. console.log("row", loc[2], loc.slice(3), err.msg); } else { console.log("batch", loc, err.msg); }}
Business-rule validation errors: HTTP 422 with detail.errors
Strict-mode business-rule failures return one entry per invalid row under detail.errors. Each entry carries the row's external_reference_id (null if the row had none) and a nested errors array of objects with exactly field and message. Several failing rows produce several entries; several failures in one row produce several objects in that row's array.
{ "detail": { "errors": [ { "external_reference_id": "row-1", "errors": [ { "field": "expected_appointment_date_time", "message": "expected_appointment_date_time must be in the past for objective CONFIRM_IE_ATTENDANCE" } ] }, { "external_reference_id": "row-2", "errors": [ { "field": "organization_name", "message": "Organization 'Acme' is not configured for tenant_id 'tenant-a'" } ] } ] }}
Iterate the entries and join each one back to its row by external_reference_id:
for row in body["detail"]["errors"]: for err in row["errors"]: print(row["external_reference_id"], err["field"], err["message"])
for (const row of body.detail.errors) { for (const err of row.errors) { console.log(row.external_reference_id, err.field, err.message); }}
Introducing the new less strict validation mode: SKIP_INVALID_ROWS
Use validation_mode: "SKIP_INVALID_ROWS" to keep valid rows even when other rows fail. This mode validates every row and skips only rows that fail; rejected rows are returned alongside accepted rows in scheduled_requests. It is also more permissive: it allows call job API requests where a few of the rows fail validation, so the remainder passes through and still makes calls without blocking the entire batch.
Every input row is checked, and only rows that pass both validation stages are eligible for scheduling.
Schema validation parses every row. Schema-invalid rows become INVALID results; business-rule validation runs on the remaining rows and reports any failures in the same format.
If at least one row passes, we create a call job, persist the accepted rows, and attempt to start its workflow. If no rows pass, we create no job, scheduled requests, or workflow.
Response shape: HTTP 200 for every mix of row results
For a valid batch structure, row validation returns HTTP 200 whether no rows, some rows, or all rows fail.
Row validation result
HTTP status
call_job_id
scheduled_requests
No row errors
200
Created job UUID
All rows accepted, normally ENQUEUED
Some row errors
200
Created job UUID
Accepted and INVALID rows in input order
All rows have errors
200
null
All rows INVALID
Every case has the same top-level fields: call_job_id, submitted_at, and scheduled_requests. The all-invalid case uses the same rejected-row objects as partial success, so clients do not switch to detail.errors when the last valid row disappears.
Accepted and invalid row fields
Schema and business-rule failures use the same INVALID row format in scheduled_requests, even when every input row fails. Each result stays in its original input position.
Rejected rows contain row_index, nullable external_reference_id and provider_office, status: "INVALID", and a non-empty errors array of {field, message} objects. They have no database IDs, are not persisted, and do not appear in later polling.
Accepted rows retain snapshot_id, scheduled_request_id, external_reference_id, provider_office, entity_id, status, and the singular error field. They normally have status: "ENQUEUED" and error: null; they do not gain row_index or an errors array.
Response examples: no, some, and all row errors
These example responses show the same top-level shape for two submitted rows, with different mixes of results:
Client handling: read scheduled_requests in every case
Use the same loop for all three cases. Inspect errors only on INVALID rows, and poll a call job only when call_job_id is not null:
# After handling any non-200 HTTP response:for row in body["scheduled_requests"]: if row["status"] == "INVALID": for err in row["errors"]: print(row["row_index"], err["field"], err["message"]) else: print(row["scheduled_request_id"], row["status"], row["error"])if body["call_job_id"] is not None: # Use this ID to track the accepted rows. print("call_job_id", body["call_job_id"])
// After handling any non-200 HTTP response:for (const row of body.scheduled_requests) { if (row.status === "INVALID") { for (const err of row.errors) { console.log(row.row_index, err.field, err.message); } } else { console.log(row.scheduled_request_id, row.status, row.error); }}if (body.call_job_id !== null) { // Use this ID to track the accepted rows. console.log("call_job_id", body.call_job_id);}
This replaces the previous all-invalid skip-mode response, which used HTTP 422 and detail.errors. Strict-mode errors and batch-level validation errors keep their existing handling.
Schema errors in the less strict SKIP_INVALID_ROWS: HTTP 422 still applies
Schema failures still return HTTP 422 with a detail list in either mode. These include an empty calls list, unknown fields or external_reference_id on only some rows. If schema errors exist, it would appear in that error list before business validation or persistence runs.