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 = stringResponse
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 totrueto 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/stopYou 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/bulkContent-Type: multipart/form-data; boundary=__boundary123
--__boundary123Content-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.