Telephony
Telephony endpoints run outbound interview calling: the company’s assigned phone numbers, capacity and availability, the calling-hours policy, and the reservations created when you schedule a call. Instead of emailing a candidate an interview link, you reserve a slot and Celper dials the candidate’s phone at the planned time.
Two things must be in place before scheduling works:
- Telephony must be enabled for the company (
company.telephonyEnabled === true). SIP-mutating routes return403 TELEPHONY_DISABLEDwhen it is off. - Calling hours must be configured for non-test candidates. Scheduling is blocked with
412 WORKING_HOURS_NOT_CONFIGUREDuntil you save a calling-hours policy viaPATCH /v1/telephony/calling-hours.
Three scopes gate this section:
| Scope | Operations |
|---|---|
telephony:read | GET numbers, clock, availability, calling-hours, a single reservation |
telephony:schedule | schedule-call, batch schedule-call, revoke-call, cancel a reservation, cancel a batch |
telephony:manage | PATCH a number, POST/PATCH the calling-hours policy |
All telephony routes share the global rate-limit bucket (60 requests / 60s).
GET /v1/telephony/calling-hours
/v1/telephony/calling-hoursReturns the company’s default calling-hours policy. defaultCallingHours is the HH:MM-HH:MM window (null if unset), defaultTimezone is the company-wide fallback timezone (null means “Automatic” — each candidate resolves its own timezone), and defaultCallingDays are ISO weekday numbers (1 = Mon … 7 = Sun; null means no weekday filtering).
Scope: telephony:read | Rate limit: global (60 requests / 60s)
curl https://api.celper.ai/v1/telephony/calling-hours -H "X-API-Key: celp_your_api_key_here"POST /v1/telephony/calling-hours
/v1/telephony/calling-hoursPreviews which firm reservations a proposed calling-hours policy would push outside the calling window. This does not save anything and never mutates reservations — use it to show conflicts before committing a change.
Scope: telephony:manage | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
start | string | Required | Local time-of-day `HH:MM` (00:00-23:59). |
end | string | Required | Local time-of-day `HH:MM`. Must be after `start` -- no wrap-around. |
timezone | string | Optional | Ignored for preview (carried only by PATCH). |
weekdays | number[] | Optional | Allowed calling days as ISO weekday numbers (1=Mon..7=Sun). When omitted, the preview reuses the stored allowed days so it reflects what a save would actually enforce. |
curl -X POST https://api.celper.ai/v1/telephony/calling-hours -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -d '{ "start": "10:00", "end": "16:00", "weekdays": [1, 2, 3, 4, 5] }'PATCH /v1/telephony/calling-hours
/v1/telephony/calling-hoursSaves the company default calling-hours policy and returns the conflicts the new policy creates. The write merges into the company telephony map — sibling fields survive. Reservations are not mutated; applying the change to affected calls is a separate batch-reschedule flow (POST /v1/candidates/batch/schedule-call with replace: true).
For timezone, undefined (field omitted) preserves the stored value, null clears it to “Automatic”, and a string sets the company-wide fallback. For weekdays, undefined preserves the stored days while a present array replaces them (deduped and sorted); an empty array is rejected with 400 (there is no clear surface for weekdays).
Scope: telephony:manage | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
start | string | Required | Local time-of-day `HH:MM` (00:00-23:59). |
end | string | Required | Local time-of-day `HH:MM`. Must be after `start`. |
timezone | string | null | Optional | Omit to preserve the stored value, `null` to clear it (Automatic), or a string to set the company-wide fallback timezone. |
weekdays | number[] | Optional | ISO weekday numbers 1-7. Omit to preserve stored days; a present array replaces the stored set. An empty array is rejected (`400`). |
curl -X PATCH https://api.celper.ai/v1/telephony/calling-hours -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -d '{
"start": "09:00",
"end": "18:00",
"timezone": "Europe/Vilnius",
"weekdays": [1, 2, 3, 4, 5]
}'GET /v1/telephony/numbers
/v1/telephony/numbersLists the outbound numbers assigned to the authenticated company. isDefault is the assignment’s defaultForOutbound flag. capabilities and capacity fall back to safe defaults ({ voice: true, sms: false }, caps 0) if the underlying phone-number document is missing.
Scope: telephony:read | Rate limit: global (60 requests / 60s)
curl https://api.celper.ai/v1/telephony/numbers -H "X-API-Key: celp_your_api_key_here"PATCH /v1/telephony/numbers/{id}
/v1/telephony/numbers/{id}Updates a number’s display label and/or whether it is the company default for outbound calls. At least one field must be provided (400 VALIDATION_ERROR otherwise). {id} is the assignment id from GET /v1/telephony/numbers; an id not assigned to this company returns 404 NOT_FOUND.
Scope: telephony:manage | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | The phone-number assignment id from `GET /v1/telephony/numbers`. |
| Parameter | Type | Required | Description |
|---|---|---|---|
displayName | string | Optional | 1-120 characters. |
defaultForOutbound | boolean | Optional | Whether this number is the company default for outbound calls. |
curl -X PATCH https://api.celper.ai/v1/telephony/numbers/pn_8f2a -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -d '{ "displayName": "Vilnius primary", "defaultForOutbound": true }'GET /v1/telephony/clock
/v1/telephony/clockCompany-scoped health view of the outbound calling system plus the company’s assigned numbers. health is the clock health discriminant (healthy, degraded, or saturated); mode is present and equal to "conservative_breaker" only while the breaker is engaged. Unlike /numbers, the per-number cap object is named caps and the default flag is defaultForOutbound (not isDefault). No global capacity or VM internals are exposed.
Scope: telephony:read | Rate limit: global (60 requests / 60s)
curl https://api.celper.ai/v1/telephony/clock -H "X-API-Key: celp_your_api_key_here"GET /v1/telephony/availability
/v1/telephony/availabilityReturns 5-minute-granularity slots with remaining outbound capacity across a window, for capacity heatmaps and slot picking. The outbound number is resolved from phoneNumberId (or the company default). remaining is min(globalCap, perNumberCap) - peakConcurrency, clamped at 0. An unresolvable number surfaces the resolver’s own error code.
Scope: telephony:read | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
start | integer | Required | Window start, epoch ms. |
end | integer | Required | Window end, epoch ms. Must be `> start` (`400 VALIDATION_ERROR` otherwise). |
durationMs | integer | Required | Interview duration, 60,000 ms to 4 h (14,400,000 ms). |
phoneNumberId | string | Optional | Explicit number; otherwise the company's resolved outbound number is used. |
channel | string | Optional | Only `outbound` (default) is supported. |
curl "https://api.celper.ai/v1/telephony/availability?start=1749038400000&end=1749045600000&durationMs=1800000&phoneNumberId=pn_8f2a" -H "X-API-Key: celp_your_api_key_here"POST /v1/candidates/{candidateId}/schedule-call
/v1/candidates/{candidateId}/schedule-callSchedules an outbound interview call for one candidate, creating a reservation that Celper dials at the planned time. Supply either an explicit window (windowStartMs + windowEndMs) or a preset (one is required). The reservation freezes the resolved timezone, calling hours, and retry policy at booking time.
Timezone is resolved in order: explicit timezone (manual) -> derived from the candidate’s phone country -> company default. A phone in a multi-timezone country with no explicit timezone returns 422 TIMEZONE_SELECTION_REQUIRED. Use phoneNumberId to pin a specific outbound number, otherwise the company’s resolved/default number is used. invitationExpirationDays (1-7, default 7) is preserved across the retry attempt and callback window.
The per-booking retryCall policy is frozen on the reservation as retryCallSnapshot. Its mode is one of OFF, SAME_DAY, or LATER_HALF; send null or omit it to disable retry for this booking. Set replace: true to replace the candidate’s existing firm reservation instead of booking alongside it — valid only against a firm reservation; any other live state returns 409 CANDIDATE_HAS_ACTIVE_RESERVATION.
force / forceReason are admin-session only: an API-key caller that sends force=true always receives 403 ADMIN_ONLY_PARAMETER.
Scope: telephony:schedule | Rate limit: global (60 requests / 60s) | Idempotent: supports Idempotency-Key header
| Parameter | Type | Required | Description |
|---|---|---|---|
candidateId | string | Required | The unique identifier of the candidate. |
| Parameter | Type | Required | Description |
|---|---|---|---|
jobPositionId | string | Required | The job position the candidate belongs to. |
windowStartMs | integer | Optional | Window start, epoch ms. Required together with `windowEndMs` unless `preset` is supplied. |
windowEndMs | integer | Optional | Window end, epoch ms. Required together with `windowStartMs` unless `preset` is supplied. |
preset | string | Optional | A scheduling preset, required unless an explicit window is supplied. One of `complete-within-15m`, `complete-within-1h`, `complete-within-4h`, `complete-within-24h`, `complete-within-48h`, `complete-by-next-business-day`. |
timezone | string | Optional | Candidate timezone override (manual). When omitted, timezone is derived from the phone, then the company default. |
timezoneSource | string | Optional | One of `phone`, `manual`, or `company-default`. |
phoneNumberId | string | Optional | Explicit outbound number assignment. Defaults to the company resolved/default number. |
invitationExpirationDays | integer | Optional | 1-7 days, default 7. Preserved across the retry attempt and callback window. |
retryCall | object | null | Optional | Per-booking retry policy `{ mode }` where `mode` is `OFF`, `SAME_DAY`, or `LATER_HALF`. Frozen on the reservation as `retryCallSnapshot`. Send `null` or omit to disable retry. |
replace | boolean | Optional | Replaces the candidate's existing firm reservation instead of booking alongside it. Only valid against a `firm` reservation. |
force / forceReason | admin only | Optional | Admin-session only. API-key callers that send `force=true` receive `403 ADMIN_ONLY_PARAMETER`. |
curl -X POST https://api.celper.ai/v1/candidates/cand_xyz789/schedule-call -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -H "Idempotency-Key: sched-cand_xyz789-2026-06-04" -d '{
"jobPositionId": "pos_abc123",
"preset": "complete-within-24h",
"timezone": "Europe/Vilnius",
"invitationExpirationDays": 5,
"retryCall": { "mode": "SAME_DAY" }
}'POST /v1/candidates/batch/schedule-call
/v1/candidates/batch/schedule-callSchedules outbound interview calls for up to 200 candidates in one request. The batch is not atomic — each candidate yields an independent outcome of scheduled, conflict, or failed. A scheduled entry carries its reservationId; a conflict or failed entry carries a reason code. Concurrency is bounded by the company clock config, and test candidates bypass the working-hours precondition (a mixed test/non-test batch is not failed wholesale).
phoneNumberStrategy controls number resolution: autoresolve (default) matches each candidate’s E.164 country to assigned numbers (least-loaded tie-break, company-default fallback); default uses the company defaultForOutbound number for every candidate; per_candidate reads perCandidateOverrides[id].phoneNumberId and falls back to autoresolve. Set replace: true to replace each candidate’s existing firm reservation rather than booking alongside it.
Scope: telephony:schedule | Rate limit: global (60 requests / 60s) | Idempotent: supports Idempotency-Key header
| Parameter | Type | Required | Description |
|---|---|---|---|
candidateIds | string[] | Required | 1-200 candidate IDs. |
jobPositionId | string | Required | The job position the candidates belong to. |
windowStartMs | integer | Optional | Window start, epoch ms. Required with `windowEndMs` unless `preset` is supplied. |
windowEndMs | integer | Optional | Window end, epoch ms. Required with `windowStartMs` unless `preset` is supplied. |
preset | string | Optional | A scheduling preset, required unless an explicit window is supplied. Same enum as the single-candidate route. |
timezone | string | Optional | Shared timezone fallback for the batch. |
invitationExpirationDays | integer | Optional | 1-7 days, default 7. |
retryCall | object | null | Optional | One retry policy `{ mode }` applied to every reservation in the batch. Omit or `null` to disable retry across the batch. No per-candidate override surface in v1. |
phoneNumberStrategy | string | Optional | `autoresolve` (default), `default`, or `per_candidate`. |
perCandidateOverrides | object | Optional | Map of `candidateId -> { phoneNumberId?, timezone? }`. |
replace | boolean | Optional | Reschedule mode. When `true`, each candidate's existing firm reservation is replaced rather than booked alongside it. |
curl -X POST https://api.celper.ai/v1/candidates/batch/schedule-call -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -H "Idempotency-Key: batch-2026-06-04-cohortA" -d '{
"candidateIds": ["cand_xyz789", "cand_def456", "cand_ghi012"],
"jobPositionId": "pos_abc123",
"preset": "complete-within-48h",
"phoneNumberStrategy": "autoresolve",
"invitationExpirationDays": 7,
"perCandidateOverrides": {
"cand_ghi012": { "timezone": "Europe/Riga" }
}
}'GET /v1/telephony/reservations/{id}
/v1/telephony/reservations/{id}Reads a single reservation document. Cross-company reads return 404 NOT_FOUND (existence is not leaked).
Scope: telephony:read | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | The reservation id. |
curl https://api.celper.ai/v1/telephony/reservations/resv_3c1d -H "X-API-Key: celp_your_api_key_here"POST /v1/telephony/reservations/{id}/cancel
/v1/telephony/reservations/{id}/cancelCancels a single reservation. The public path does not force-cancel in-flight calls — a reservation already mid-dispatch (a human may be on the line) returns 409 CANDIDATE_HAS_ACTIVE_RESERVATION; use the batch-cancel path or revoke-by-candidate for those. On success the reservation flags are flipped and teardown of any linked LiveKit session is requested. Cross-company ids return 404 NOT_FOUND.
Scope: telephony:schedule | Rate limit: global (60 requests / 60s) | Idempotent: supports Idempotency-Key header
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | The reservation id. |
curl -X POST https://api.celper.ai/v1/telephony/reservations/resv_3c1d/cancel -H "X-API-Key: celp_your_api_key_here" -H "Idempotency-Key: cancel-resv_3c1d"POST /v1/telephony/batches/{batchId}/cancel
/v1/telephony/batches/{batchId}/cancelCancels every reservation tagged with batchId that is still cancellable. Only reservations in firm or dispatching are cancelled — dispatching is accepted here (mid-SIP-setup, no human on the line yet) even though the single-reservation path rejects it. Reservations in any other state (ringing, active, completed, cancelled, expired, failed, no_answer, sip_failed) are reported in skipped with a reason. A batchId with no reservations for this company returns 404 NOT_FOUND.
Scope: telephony:schedule | Rate limit: global (60 requests / 60s) | Idempotent: supports Idempotency-Key header
| Parameter | Type | Required | Description |
|---|---|---|---|
batchId | string | Required | The batch id returned by `POST /v1/candidates/batch/schedule-call`. |
curl -X POST https://api.celper.ai/v1/telephony/batches/batch_91af/cancel -H "X-API-Key: celp_your_api_key_here" -H "Idempotency-Key: cancel-batch_91af"POST /v1/candidates/{candidateId}/revoke-call
/v1/candidates/{candidateId}/revoke-callRevokes an active phone-invite for a candidate, or cancels the candidate’s linked firm reservation. This is the candidate-addressed counterpart to POST /v1/telephony/reservations/{id}/cancel — use it when you hold a candidateId rather than a reservationId. Telephony does not need to be enabled for this route; revoke must always be able to clean up state.
If the candidate is in invited_call, the SIP-aware revoke runs (status flips back to new, the session is revoked, the expiration task is cancelled, the LiveKit allowlist is reconciled) and any linked reservation is cancelled opportunistically. Otherwise, if a linked firm reservation exists, it is cancelled and the candidate’s scheduling pointer is cleared. If neither applies, the route returns 409 INVALID_STATUS_TRANSITION. companyId is taken from the authenticated context; any body companyId is ignored, so an API key for company A cannot address company B’s candidate.
For API-key callers, the response uses the standard apiSuccess envelope with rate-limit headers (errors use the standard { success, error, message, requestId } envelope). The data shape depends on what was revoked: an invited_call revoke returns { status: "new", revoked: true, reconciled, fallbackEnqueued }, while a firm-reservation cancel returns { ok: true, revoked: true, cancelledReservationId, priorState }.
Scope: telephony:schedule | Rate limit: global (60 requests / 60s)
| Parameter | Type | Required | Description |
|---|---|---|---|
candidateId | string | Required | The unique identifier of the candidate. |
| Parameter | Type | Required | Description |
|---|---|---|---|
jobPositionId | string | Required | The job position the candidate belongs to. May also be supplied as a `?jobPositionId=` query param. |
curl -X POST https://api.celper.ai/v1/candidates/cand_xyz789/revoke-call -H "X-API-Key: celp_your_api_key_here" -H "Content-Type: application/json" -d '{ "jobPositionId": "pos_abc123" }'