Skip to content

Jobs

Create Job

For bulk job creation, see Bulk-Import Jobs.

POST /campaigns/:campaignID/jobs

Request body:

interface CreateJobBody {
ContactPhoneNumber: string // e164 format (e.g. +12223334444)
// Set a specific phone number to make this phone call from. Only numbers that were whitelisted in the campaign can be used here.
// If not set, the campaign's default caller ID will be used.
// Must be in e164 format (e.g. +12223334444)
CallerID?: string
ContactName?: string
// extra NON-PII data, that you can attach to a job. This can be used for templating messages, reporting & analytics, and looking up internal customers post-PII deletion
Metadata?: Record<string, any>
// Data for templating and providing the agent additional context. Will be deleted when the PII is deleted.
Data?: Record<string, any>
// Various overrides that are otherwise set at the Campaign level
Overrides: JobOverrides
// List of campaign variants IDs to to use for this job.
// If undefined, variants will be automatically selected.
// To not use any variants, set this to an empty array
CampaignVariants?: Array<string>
// Whether this is a test job. Used for filtering in the API and analytics.
// Setting this to true will disable encryption and redaction, to help with debugging!
TestJob?: boolean
}
interface JobOverrides {
/*
* If the user disconnects before the end of the conversation,
* consider this an "opt-out" and fail (possibly partially) the Job
*/
HangUpIsOptOut?: bool
/*
* If set to true, call audio will not be recorded. Transcriptions are not affected by this.
*/
DisableRecording?: bool
MaxCallSeconds?: number // int64
MaxRingSeconds?: number // int64
Integrations?: Record<string, any>
Constraints?: {
/*
* minimum delay since creation of the job before
* a call can be schedule (prevents immediate calling).
*/
MinCreationDelay?: Duration
// Disables scheduling jitter, useful if you need the call to go right now
DisableJitter?: bool
// Minimum duration between call attempts.
MinDurationBetweenCalls?: Duration
// max number of calls we can ever attempt. If 0, no limit
MaxTotalCalls?: number
/*
* Limit the number of call attempts within a time interval.
* E.g. can only call a max of 3 times in a 5-day period
*/
MaxAttemptsInInterval?: {
Count: int
Duration: Duration
}
/*
* TZ identifier format (e.g. 'America/Los_Angeles'),
* see `Time Zones` API reference page.
* Required when using time range
*/
TimeZone?: string // required with TimeRange
// will only call at times matching at least one of these rules
TimeRules?: Array<TimeRule>
}
Redaction?: {
redact?: string[]
keep?: string[]
}
// Speech-to-Text (STT) settings
SST?: {
deepgram?: {
options?: {
language?: string
keywords?: string[]
// all options options from here are available: https://developers.deepgram.com/reference/listen-live#body-params
// but you should only use the ones from above, since other ones might break the call
}
}
}
}
interface TimeRule {
TimeRanges: Array<TimeRange>
// Two-letter abbreviation for days of week that the rule applies to
// If empty, the rule applies to all days
Weekdays?: Array<`Su` | `Mo` | `Tu` | `We` | `Th` | `Fr` | `Sa`>
}
interface TimeRange {
Start: string // 24-hour format (e.g., "09:00")
End: string // 24-hour format (e.g., "17:00")
}
// a time duration expressed in shorthand notation,
// e.g. '5m' (5 minute), '12h30m' (12 hours and 30 minutes), etc.
type Duration = string

Response

201 application/json:

interface Job {
JobID: string
}

Get Job Information

GET /campaigns/:campaignID/jobs/:jobID

Response

200 application/json:

interface JobResponse {
OrgID: string
CampaignID: string
ID: string
Data: JobOverrides & {
PhoneNumber: string
PreviousCallsSummary?: string
// final job analysis, based on campaign analysis schema
Analysis?: CallAnalysis
ContactName?: string
// The version of the campaign that this job is using.
// Gets set on first call, and then never changes
CampaignVersion: number | null
Calls?: {
ID: string
CreatedAt: string
State: "creating" | "active" | "ended" | "failed"
EndedAt?: string // RFC3339 date-time
EndedReason?:
| "assistant_error"
| "assistant_hung_up"
| "contact_hung_up"
| "contact_busy"
| "contact_did_not_answer"
| "call_timeout"
ConnectedAt?: string // RFC3339 date-time
DisconnectedAt?: string // RFC3339 date-time
Error?: string
Messages: LLMMessage[]
// URLs to audio recordings
// Three files: .../input.mp3, .../output.mp3, and .../merged.mp3 for getting each channel separately or both merged respectively
// These URLs point to our API and this need authentication to access them: Either use the API key as a bearer token or append `?apiKey=key_XXXXXXXX` to the URL.
// The latter is useful if you want to use the media file in a browser. Note that merged.mp3 only exists for calls after Feb. 2025
Recordings?: string[]
}[]
// Analysis run after each call (analysis includes all prior calls)
CallAnalyses?: {
[call_id: string]: CallAnalysis
}
OutputData?: Record<string, any>
AuditLogEntries?: string[] // List of audio log entry IDs for this Job
State:
| "created"
| "scheduled"
| "calling"
| "completed"
| "failed"
| "canceled"
| "deleting"
| "deleted"
FailureReason?:
| "campaign_objective_missed"
| "canceled"
| "max_attempts_exceeded"
| "opted_out"
| "technical_error"
| "wrong_contact"
FailureMessage?: string // provides more detail when a `FailureReason` is present
Metadata?: Record<string, any>
Data?: Record<string, any>
NextCallMS?: number // int64, when the next call is scheduled (if scheduled)
TestJob?: boolean // whether this is a test job. Only exists as `true` if a test job.
}
Metrics: {
TotalCallAttempts: number
TotalCallSeconds: number // amount of time, including ringing
TotalCallConversationalSeconds: number // total incurred conversational time
}
Events?: {
// analytics events, if events=1 query param is enabled
EventType: string
CreatedMS: number
Properties: Record<string, any>
}[]
// List of campaign variants IDs that apply to this job. Unless explicitly set, this will be undefined until the first call is made.
CampaignVariants?: Array<string>
CreatedAt: string // ISO 8601 (RFC3339)
}
interface CallAnalysis {
CallSummary: string
Data: Record<string, any> // from your campaign's analysis schema
ConversationEndReason:
| "conversation_complete"
| "call_dropped"
| "user_refusal"
| "left_voicemail"
| "abandoned_answering_machine"
| "wrong_contact"
| "no_answer"
| "other_unsuccessful"
AskedIfBotOrAI: boolean // if the user asked if the agent was a bot or AI
ExpressedEmotion?: string | null // if a strong emotion was expressed, it will be described here. Only included if audio analysis is used
TranscriptionErrors?: string[] | null // if the audio analysis finds any issues with the transcription used
}

List Jobs

GET /campaigns/:campaignID/jobs

Returns the next up to 100 Jobs

Query parameters:

  • after: ID of the Job to start listing after (non-inclusive), used for pagination.
  • by_time: Set to true to list by most recently updated (descending)
    • before_ms: List from this time backwards in unix milliseconds. Must be an integer, or omitted.

Response

200 application/json:

interface JobsList {
Jobs: Array<{
ID: string
PhoneNumber: string
ContactName: string
State: string
CreatedAt: number // unix ms
UpdatedAt: number // unix ms
}>
}

Search for Jobs

GET /campaigns/:campaignID/jobs/search

Query parameters:

  • search: Job ID or phone number

Response

See List Jobs

Delete Job PII

DELETE /campaigns/:campaignID/jobs/:jobID

For tracking purposes, Jobs cannot be fully deleted (we keep a reference of completion, and metadata for Campaign reporting).

You can (and should) delete PII when you are done with any specific Job data.

This removes recordings, transcripts, the Data field, and any other data that may contain PII.

Response

202: Job deleting

PII deletion is run as a background job, and is idempotent: Subsequent calls to delete the Job PII will always return a 202 status, and it’s safe to call multiple times even if already deleted.

Cancel Job

POST /campaigns/:campaignID/jobs/:jobID/cancel

If a Job has not completed yet, you can cancel it.

Canceling a Job does not delete PII, that must be done as a secondary action.

Canceling a completed or failed job will return an error.

Response

202: Job canceled

409: Job not running

Job cancellation happens asynchronously as indicated by the 202 response status code.

Reschedule a job

POST /campaigns/:campaignID/jobs/:jobID/reschedule

Request body:

interface RescheduleJobRequest {
// Unix timestamp (in seconds) of when to start the job's next call.
// Set to a negative value for "now".
CallAtUnixSeconds: number
}

Response

202: Job rescheduled

400: Job in wrong state (only works when currently scheduled)

Update a job

POST /campaigns/:campaignID/jobs/:jobID/update

Update a job’s Data, Metadata, or Constraints.

Request body:

interface UpdateJobRequest {
Data?: Record<string, any>
Metadata?: Record<string, any>
Constraints?: {/* ... See CreateJob ... */}
}

When updating Constraints, the system will automatically trigger rescheduling of the next call if needed.

Response

202: Job updated

404: Job not found

409: Job not running, cannot update

Listen-in to a live call

While a job’s call is active, you can list to the live audio streams of the call.

Open websocket connections to the following URLs:

  • /campaigns/:campaignID/jobs/:jobID/calls/:callID/stream-audio-bot
  • /campaigns/:campaignID/jobs/:jobID/calls/:callID/stream-audio-user

The audio streams will be sent as binary messages containing raw PCM data.

For the bot stream, that’s 24kHz, 16-bit, mono audio. For the user stream, that’s 16kHz, 16-bit, mono audio.

Stopping a running call

If while live-listening into a call, you decide it has to be stopped immediately, you can use this endpoint to do so.

POST /campaigns/:campaignID/jobs/:jobID/calls/:callID/stop

You can optionally provide a reason for why the call was stopped. (Don’t forget to set Content-Type: application/json if you do)

interface StopCallReq {
Reason: string
}

Bulk-Import Jobs

Jobs can be bulk-uploaded using either CSV or Newline-Delimited JSON (NDJSON). Jobs will be created asynchronously in the background, so you will see them load in over time, if you’re uploading a large number at once.

As a result, validation happens in the background, so if there are any issues with creating a Job they will immediately fail with the reason invalid_configuration and report the error.

You can also bulk upload from the dashboard as well!

You need to upload the data as multipart/form-data with a file called file - max file size 10MB.

NDJSON

When using NDJSON, set the content-type to application/x-ndjson. And then every line of the file should be (minified) CreateJobBody JSONs.

CSV

This is an example for a CSV upload, you can use any top-level field of CreateJobBody as a column. For optional properties, you can omit the column entirely, or leave the cell blank.

Complex fields like Metadata should be JSON. Note that JSON inside CSVs need to be properly escaped. (see the double double-quotes in the example below).

Request

POST /campaigns/test-campaign/jobs/bulk
Content-Type: multipart/form-data; boundary=__boundary123
--__boundary123
Content-Disposition: form-data; name="file"; filename="test.csv"
Content-Type: text/csv
"ContactName","ContactPhoneNumber","CampaignVariants","Metadata"
"John Doe 1","+0123456789012",,"{""foo"":""bar""}"
"John Doe 2","+0123456789012","[""cv_variant1""]","{}"
--__boundary123--

Response

202 Bulk job creation started.

{
"ImportID": "req_4EDpKRV6mCMFs3AxQ1GewB"
}

This import ID can be used to reconcile the import later. Each job created from the bulk import the following extra metadata added:

  • __cobbery_ImportID: the import ID from above
  • __cobbery_RowID: the row ID (i.e. line number excluding headers) of the uploaded file

We immediately validate the first few rows, but if any later lines contain errors, we will start the import job, and as soon as the job is created, it will immediately fail. You’ll get webhooks for these jobs as usual.