Skip to Content
API EndpointsTelephony

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 return 403 TELEPHONY_DISABLED when it is off.
  • Calling hours must be configured for non-test candidates. Scheduling is blocked with 412 WORKING_HOURS_NOT_CONFIGURED until you save a calling-hours policy via PATCH /v1/telephony/calling-hours.

Three scopes gate this section:

ScopeOperations
telephony:readGET numbers, clock, availability, calling-hours, a single reservation
telephony:scheduleschedule-call, batch schedule-call, revoke-call, cancel a reservation, cancel a batch
telephony:managePATCH 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

GET/v1/telephony/calling-hours

Returns 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"
200Success
{
  "success": true,
  "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "data": {
    "defaultCallingHours": {
      "start": "09:00",
      "end": "18:00"
    },
    "defaultTimezone": "Europe/Vilnius",
    "defaultCallingDays": [
      1,
      2,
      3,
      4,
      5
    ]
  }
}

POST /v1/telephony/calling-hours

POST/v1/telephony/calling-hours

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

Body Parameters
ParameterTypeRequiredDescription
startstringRequiredLocal time-of-day `HH:MM` (00:00-23:59).
endstringRequiredLocal time-of-day `HH:MM`. Must be after `start` -- no wrap-around.
timezonestringOptionalIgnored for preview (carried only by PATCH).
weekdaysnumber[]OptionalAllowed 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] }'
200Success
{
  "success": true,
  "requestId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "data": {
    "conflicts": [
      {
        "reservationId": "resv_3c1d",
        "candidateId": "cand_xyz789",
        "plannedAttemptAt": 1749042000000
      }
    ]
  }
}

PATCH /v1/telephony/calling-hours

PATCH/v1/telephony/calling-hours

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

Body Parameters
ParameterTypeRequiredDescription
startstringRequiredLocal time-of-day `HH:MM` (00:00-23:59).
endstringRequiredLocal time-of-day `HH:MM`. Must be after `start`.
timezonestring | nullOptionalOmit to preserve the stored value, `null` to clear it (Automatic), or a string to set the company-wide fallback timezone.
weekdaysnumber[]OptionalISO 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]
}'
200Success
{
  "success": true,
  "requestId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "data": {
    "defaultCallingHours": {
      "start": "09:00",
      "end": "18:00"
    },
    "defaultTimezone": "Europe/Vilnius",
    "defaultCallingDays": [
      1,
      2,
      3,
      4,
      5
    ],
    "conflicts": []
  }
}

GET /v1/telephony/numbers

GET/v1/telephony/numbers

Lists 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"
200Success
{
  "success": true,
  "requestId": "d4e5f6a7-b8c9-0123-defa-234567890123",
  "data": {
    "numbers": [
      {
        "id": "pn_8f2a",
        "e164": "+37052071234",
        "displayName": "Vilnius outbound",
        "capabilities": {
          "voice": true,
          "sms": false
        },
        "capacity": {
          "maxInboundConcurrent": 4,
          "maxOutboundConcurrent": 8
        },
        "isDefault": true
      }
    ]
  }
}

PATCH /v1/telephony/numbers/{id}

PATCH/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)

Path Parameters
ParameterTypeRequiredDescription
idstringRequiredThe phone-number assignment id from `GET /v1/telephony/numbers`.
Body Parameters
ParameterTypeRequiredDescription
displayNamestringOptional1-120 characters.
defaultForOutboundbooleanOptionalWhether 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 }'
200Success
{
  "success": true,
  "requestId": "e5f6a7b8-c9d0-1234-efab-345678901234",
  "data": {
    "number": {
      "id": "pn_8f2a",
      "displayName": "Vilnius primary",
      "defaultForOutbound": true
    }
  }
}
404Not found

GET /v1/telephony/clock

GET/v1/telephony/clock

Company-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"
200Success
{
  "success": true,
  "requestId": "a7b8c9d0-e1f2-3456-abcd-567890123456",
  "data": {
    "assignedNumbers": [
      {
        "id": "pn_8f2a",
        "e164": "+37052071234",
        "displayName": "Vilnius outbound",
        "caps": {
          "maxInboundConcurrent": 4,
          "maxOutboundConcurrent": 8
        },
        "defaultForOutbound": true
      }
    ],
    "health": "healthy"
  }
}

GET /v1/telephony/availability

GET/v1/telephony/availability

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

Query Parameters
ParameterTypeRequiredDescription
startintegerRequiredWindow start, epoch ms.
endintegerRequiredWindow end, epoch ms. Must be `> start` (`400 VALIDATION_ERROR` otherwise).
durationMsintegerRequiredInterview duration, 60,000 ms to 4 h (14,400,000 ms).
phoneNumberIdstringOptionalExplicit number; otherwise the company's resolved outbound number is used.
channelstringOptionalOnly `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"
200Success
{
  "success": true,
  "requestId": "b8c9d0e1-f2a3-4567-bcde-678901234567",
  "data": {
    "slots": [
      {
        "startMs": 1749038400000,
        "endMs": 1749040200000,
        "remaining": 7
      },
      {
        "startMs": 1749038700000,
        "endMs": 1749040500000,
        "remaining": 6
      }
    ],
    "globalCap": 40,
    "perNumberCap": 8,
    "phoneNumberId": "pn_8f2a",
    "capacityPoolId": "pool_lt"
  }
}

POST /v1/candidates/{candidateId}/schedule-call

POST/v1/candidates/{candidateId}/schedule-call

Schedules 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

Path Parameters
ParameterTypeRequiredDescription
candidateIdstringRequiredThe unique identifier of the candidate.
Body Parameters
ParameterTypeRequiredDescription
jobPositionIdstringRequiredThe job position the candidate belongs to.
windowStartMsintegerOptionalWindow start, epoch ms. Required together with `windowEndMs` unless `preset` is supplied.
windowEndMsintegerOptionalWindow end, epoch ms. Required together with `windowStartMs` unless `preset` is supplied.
presetstringOptionalA 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`.
timezonestringOptionalCandidate timezone override (manual). When omitted, timezone is derived from the phone, then the company default.
timezoneSourcestringOptionalOne of `phone`, `manual`, or `company-default`.
phoneNumberIdstringOptionalExplicit outbound number assignment. Defaults to the company resolved/default number.
invitationExpirationDaysintegerOptional1-7 days, default 7. Preserved across the retry attempt and callback window.
retryCallobject | nullOptionalPer-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.
replacebooleanOptionalReplaces the candidate's existing firm reservation instead of booking alongside it. Only valid against a `firm` reservation.
force / forceReasonadmin onlyOptionalAdmin-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" }
}'
201Reservation created
{
  "success": true,
  "requestId": "c9d0e1f2-a3b4-5678-cdef-789012345678",
  "data": {
    "reservationId": "resv_3c1d",
    "state": "firm",
    "windowStartMs": 1749038400000,
    "targetEndMs": 1749124800000,
    "estimatedDurationMs": 1920000,
    "plannedAttemptAt": 1749042000000,
    "earliestDispatchByMs": 1749038400000,
    "latestDispatchByMs": 1749122880000,
    "estimatedCompletionByMs": 1749124800000,
    "scheduledCallExpiresAtMs": 1749126600000,
    "phoneNumber": "pn_8f2a",
    "timezone": "Europe/Vilnius",
    "appliedCallingHours": {
      "start": "09:00",
      "end": "18:00"
    },
    "appliedCallingDays": [
      1,
      2,
      3,
      4,
      5
    ],
    "retryCallSnapshot": {
      "enabled": true,
      "mode": "SAME_DAY"
    },
    "invitationExpirationDays": 5
  }
}
412Calling hours not configured
422Timezone selection required
429No admittable capacity
403force is admin-only

POST /v1/candidates/batch/schedule-call

POST/v1/candidates/batch/schedule-call

Schedules 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

Body Parameters
ParameterTypeRequiredDescription
candidateIdsstring[]Required1-200 candidate IDs.
jobPositionIdstringRequiredThe job position the candidates belong to.
windowStartMsintegerOptionalWindow start, epoch ms. Required with `windowEndMs` unless `preset` is supplied.
windowEndMsintegerOptionalWindow end, epoch ms. Required with `windowStartMs` unless `preset` is supplied.
presetstringOptionalA scheduling preset, required unless an explicit window is supplied. Same enum as the single-candidate route.
timezonestringOptionalShared timezone fallback for the batch.
invitationExpirationDaysintegerOptional1-7 days, default 7.
retryCallobject | nullOptionalOne 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.
phoneNumberStrategystringOptional`autoresolve` (default), `default`, or `per_candidate`.
perCandidateOverridesobjectOptionalMap of `candidateId -> { phoneNumberId?, timezone? }`.
replacebooleanOptionalReschedule 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" }
  }
}'
200Batch result
{
  "success": true,
  "requestId": "b4c5d6e7-f8a9-0123-bcde-234567890123",
  "data": {
    "batchId": "batch_91af",
    "reservations": [
      {
        "candidateId": "cand_xyz789",
        "reservationId": "resv_11aa22bb",
        "status": "scheduled"
      },
      {
        "candidateId": "cand_def456",
        "reservationId": "resv_33cc44dd",
        "status": "scheduled"
      },
      {
        "candidateId": "cand_ghi012",
        "status": "conflict",
        "reason": "NUMBER_AT_CAP"
      }
    ]
  }
}

GET /v1/telephony/reservations/{id}

GET/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)

Path Parameters
ParameterTypeRequiredDescription
idstringRequiredThe reservation id.
curl https://api.celper.ai/v1/telephony/reservations/resv_3c1d -H "X-API-Key: celp_your_api_key_here"
200Success
{
  "success": true,
  "requestId": "c5d6e7f8-a9b0-1234-cdef-345678901234",
  "data": {
    "reservation": {
      "reservationId": "resv_3c1d",
      "batchId": null,
      "companyId": "comp_acme_2f8a",
      "candidateId": "cand_xyz789",
      "jobPositionId": "pos_abc123",
      "channel": "outbound",
      "phoneNumberId": "pn_8f2a",
      "state": "firm",
      "windowStartMs": 1749038400000,
      "targetEndMs": 1749124800000,
      "latestDispatchByMs": 1749122880000,
      "appliedCallingHours": {
        "start": "09:00",
        "end": "18:00"
      },
      "appliedCallingDays": [
        1,
        2,
        3,
        4,
        5
      ]
    }
  }
}
404Not found

POST /v1/telephony/reservations/{id}/cancel

POST/v1/telephony/reservations/{id}/cancel

Cancels 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

Path Parameters
ParameterTypeRequiredDescription
idstringRequiredThe 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"
200Success
{
  "success": true,
  "requestId": "e7f8a9b0-c1d2-3456-efab-567890123456",
  "data": {
    "cancelled": true
  }
}
409Reservation mid-dispatch

POST /v1/telephony/batches/{batchId}/cancel

POST/v1/telephony/batches/{batchId}/cancel

Cancels 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

Path Parameters
ParameterTypeRequiredDescription
batchIdstringRequiredThe 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"
200Success
{
  "success": true,
  "requestId": "a9b0c1d2-e3f4-5678-abcd-789012345678",
  "data": {
    "cancelled": [
      "resv_11aa22bb",
      "resv_33cc44dd"
    ],
    "skipped": [
      {
        "id": "resv_55ee66ff",
        "reason": "state=active_not_cancellable"
      }
    ]
  }
}
404No reservations for batch

POST /v1/candidates/{candidateId}/revoke-call

POST/v1/candidates/{candidateId}/revoke-call

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

Path Parameters
ParameterTypeRequiredDescription
candidateIdstringRequiredThe unique identifier of the candidate.
Body Parameters
ParameterTypeRequiredDescription
jobPositionIdstringRequiredThe 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" }'
200Invite revoked
{
  "success": true,
  "requestId": "c1d2e3f4-a5b6-7890-cdef-901234567890",
  "data": {
    "status": "new",
    "revoked": true,
    "reconciled": true,
    "fallbackEnqueued": false
  }
}
409Nothing to cancel
Last updated on