curl --request POST \
--url https://api.upliftai.org/v1/campaigns \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Edversity cybersecurity intake — August",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": {
"assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": [
"no_answer",
"busy",
"voicemail"
],
"backoffMinutes": [
15,
120
]
}
},
"schedule": {
"callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
}
}
}
}
'import requests
url = "https://api.upliftai.org/v1/campaigns"
payload = {
"name": "Edversity cybersecurity intake — August",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": { "assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": ["no_answer", "busy", "voicemail"],
"backoffMinutes": [15, 120]
}
},
"schedule": { "callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
} }
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Edversity cybersecurity intake — August',
mode: 'continuous',
config: {
goal: 'lead',
assistant: {assistantId: '452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx'},
dialing: {
lines: 1,
fromNumber: '+924238900100',
retryPolicy: {
maxAttempts: 3,
retryOn: ['no_answer', 'busy', 'voicemail'],
backoffMinutes: [15, 120]
}
},
schedule: {callingWindow: {from: '10:00', to: '19:00', tz: 'Asia/Karachi'}}
}
})
};
fetch('https://api.upliftai.org/v1/campaigns', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.upliftai.org/v1/campaigns",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Edversity cybersecurity intake — August',
'mode' => 'continuous',
'config' => [
'goal' => 'lead',
'assistant' => [
'assistantId' => '452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
],
'dialing' => [
'lines' => 1,
'fromNumber' => '+924238900100',
'retryPolicy' => [
'maxAttempts' => 3,
'retryOn' => [
'no_answer',
'busy',
'voicemail'
],
'backoffMinutes' => [
15,
120
]
]
],
'schedule' => [
'callingWindow' => [
'from' => '10:00',
'to' => '19:00',
'tz' => 'Asia/Karachi'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.upliftai.org/v1/campaigns"
payload := strings.NewReader("{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.upliftai.org/v1/campaigns")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.upliftai.org/v1/campaigns")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}"
response = http.request(request)
puts response.read_body{
"campaignId": "5170facc-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"organizationId": "a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"projectId": "c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Edversity cybersecurity intake — August",
"state": "draft",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": {
"assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": [
"no_answer",
"busy",
"voicemail"
],
"backoffMinutes": [
15,
120
]
}
},
"schedule": {
"callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
}
}
},
"trigger": {
"kind": "list"
},
"steps": [
"call"
],
"createdAt": "2026-08-18T05:20:28.892Z",
"updatedAt": "2026-08-18T05:20:28.892Z",
"version": 1
}{
"message": "Unknown campaign mode: hybrid",
"error": "Bad Request",
"statusCode": 400
}{
"message": "invalid authorization",
"error": "Unauthorized",
"statusCode": 401
}{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}{
"message": "Contact list not found: 00000000-0000-4000-8000-000000000000",
"error": "Not Found",
"statusCode": 404
}Create a campaign
The campaign lands in draft and dials nothing until you launch it. You don’t need the full config up front: a draft can be created without an assistant.assistantId and updated field by field until launch, which is where missing pieces are rejected. What you do send is checked now: a contacts.listId that doesn’t exist fails this request. All the ways contacts get into a campaign: Contacts. The one choice you can’t revisit is mode — bounded or continuous is fixed at creation.
curl --request POST \
--url https://api.upliftai.org/v1/campaigns \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Edversity cybersecurity intake — August",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": {
"assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": [
"no_answer",
"busy",
"voicemail"
],
"backoffMinutes": [
15,
120
]
}
},
"schedule": {
"callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
}
}
}
}
'import requests
url = "https://api.upliftai.org/v1/campaigns"
payload = {
"name": "Edversity cybersecurity intake — August",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": { "assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": ["no_answer", "busy", "voicemail"],
"backoffMinutes": [15, 120]
}
},
"schedule": { "callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
} }
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Edversity cybersecurity intake — August',
mode: 'continuous',
config: {
goal: 'lead',
assistant: {assistantId: '452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx'},
dialing: {
lines: 1,
fromNumber: '+924238900100',
retryPolicy: {
maxAttempts: 3,
retryOn: ['no_answer', 'busy', 'voicemail'],
backoffMinutes: [15, 120]
}
},
schedule: {callingWindow: {from: '10:00', to: '19:00', tz: 'Asia/Karachi'}}
}
})
};
fetch('https://api.upliftai.org/v1/campaigns', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.upliftai.org/v1/campaigns",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Edversity cybersecurity intake — August',
'mode' => 'continuous',
'config' => [
'goal' => 'lead',
'assistant' => [
'assistantId' => '452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
],
'dialing' => [
'lines' => 1,
'fromNumber' => '+924238900100',
'retryPolicy' => [
'maxAttempts' => 3,
'retryOn' => [
'no_answer',
'busy',
'voicemail'
],
'backoffMinutes' => [
15,
120
]
]
],
'schedule' => [
'callingWindow' => [
'from' => '10:00',
'to' => '19:00',
'tz' => 'Asia/Karachi'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.upliftai.org/v1/campaigns"
payload := strings.NewReader("{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.upliftai.org/v1/campaigns")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.upliftai.org/v1/campaigns")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Edversity cybersecurity intake — August\",\n \"mode\": \"continuous\",\n \"config\": {\n \"goal\": \"lead\",\n \"assistant\": {\n \"assistantId\": \"452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n },\n \"dialing\": {\n \"lines\": 1,\n \"fromNumber\": \"+924238900100\",\n \"retryPolicy\": {\n \"maxAttempts\": 3,\n \"retryOn\": [\n \"no_answer\",\n \"busy\",\n \"voicemail\"\n ],\n \"backoffMinutes\": [\n 15,\n 120\n ]\n }\n },\n \"schedule\": {\n \"callingWindow\": {\n \"from\": \"10:00\",\n \"to\": \"19:00\",\n \"tz\": \"Asia/Karachi\"\n }\n }\n }\n}"
response = http.request(request)
puts response.read_body{
"campaignId": "5170facc-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"organizationId": "a12b7e74-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"projectId": "c782a4ec-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "Edversity cybersecurity intake — August",
"state": "draft",
"mode": "continuous",
"config": {
"goal": "lead",
"assistant": {
"assistantId": "452dda41-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"dialing": {
"lines": 1,
"fromNumber": "+924238900100",
"retryPolicy": {
"maxAttempts": 3,
"retryOn": [
"no_answer",
"busy",
"voicemail"
],
"backoffMinutes": [
15,
120
]
}
},
"schedule": {
"callingWindow": {
"from": "10:00",
"to": "19:00",
"tz": "Asia/Karachi"
}
}
},
"trigger": {
"kind": "list"
},
"steps": [
"call"
],
"createdAt": "2026-08-18T05:20:28.892Z",
"updatedAt": "2026-08-18T05:20:28.892Z",
"version": 1
}{
"message": "Unknown campaign mode: hybrid",
"error": "Bad Request",
"statusCode": 400
}{
"message": "invalid authorization",
"error": "Unauthorized",
"statusCode": 401
}{
"message": "Forbidden resource",
"error": "Forbidden",
"statusCode": 403
}{
"message": "Contact list not found: 00000000-0000-4000-8000-000000000000",
"error": "Not Found",
"statusCode": 404
}Authorizations
Project API key (sk_api_…).
Body
1 - 120Fixed at creation. bounded snapshots the contact list at launch and completes once the queue empties; continuous keeps accepting appended contacts and ends only when you close it.
bounded, continuous Returned exactly as stored — a section never set is absent rather than filled in with defaults.
Show child attributes
Show child attributes
Response
The campaign as stored, in draft.
120Where the campaign is in its lifecycle:
draft— created, nothing dialsscheduled— the platform launches it atconfig.schedule.scheduledForlaunching— the contact list is being materialized into a run; transientlaunch_failed— materialization failed;lastErrorsays which phase, and launching again retries itrunning— the dialer is placing callspaused— no new dials; calls already in flight run to their enddraining— closed to new work, waiting only on in-flight callscompleted— terminal
draft, scheduled, launching, launch_failed, running, paused, draining, completed Fixed at create. bounded snapshots the contact list at launch and completes itself once the queue empties; continuous keeps accepting appended contacts and completes only after a close. Absent on older campaigns; read that as bounded.
bounded, continuous Returned exactly as stored — a section never set is absent rather than filled in with defaults.
Show child attributes
Show child attributes
When the campaign was closed to new work. Continuous campaigns only; closing does not change state, so expect running until the drain begins.
The single run created at first launch; a retried launch reuses it.
First launch. Unchanged by pauses, resumes and launch retries.
Why the last launch failed. Cleared by a launch that gets further.
Show child attributes
Show child attributes
Set when the platform acts on its own, and says why. Expect Paused: out of credits, or Paused: assistant version not found when the campaign's assistant was deleted. The assistant can't be changed after launch, so a resume won't fix that one. Cleared on resume.
"Paused: out of credits"
True once any call has been graded, after which config.scorecard is frozen. Absent until then.
How the campaign takes work. Always {"kind": "list"} today.
Show child attributes
Show child attributes
What each contact goes through. Always ["call"] today.
Increments on every accepted write — compare it across reads to tell a real change from an unchanged poll.
