Mon-Fri 10am - 6pm
Getting Started
Base URL for your account: https://cpaas.studio/api/v1
- Create an API key: Administration → API Key → Add Key. Copy the plain key shown in the modal — it will not be shown again.
- Upload an audio asset, or use one already approved in your Audio Library.
- Poll Audio Status until status is approved — required before the asset can be used in a broadcast (unless your key auto-approves).
- Check your balance for the module your key is scoped to.
- Send a broadcast.
- Poll the broadcast's number status until it settles.
Authentication
Every request must carry an API key, created from your account's Administration → API Key screen. The key is shown to you exactly once at creation. It can be supplied any of the following ways — the first one found wins, checked in this order:
| # | Transport | Example |
|---|---|---|
| 1 | X-API-KEY header recommended | X-API-KEY: cpk_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| 2 | Authorization: Bearer header | Authorization: Bearer cpk_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| 3 | api_key / apikey / key parameter | ?api_key=cpk_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (query, form, or JSON body) |
GET/POST equivalence
Every endpoint accepts both GET and POST for the exact same
operation — never PUT, PATCH, or DELETE.
Parameters can be sent as a GET query string, a POST form body, or a POST JSON body —
all equivalent for every endpoint that isn't a file upload.
Response envelope
Every response — success or failure, on every endpoint — has one of these three shapes.
Success:
{
"success": true,
"message": "OK",
"data": {"...": "..."},
"request_id": "9c4a2e3e-9b7b-4a1e-8f39-2a3f6b7c9d10"
}
Failure (any code other than REQ_VALIDATION_FAILED):
{
"success": false,
"message": "A short, human-readable description of what went wrong.",
"error": {"code": "SOME_ERROR_CODE", "details": null},
"request_id": "9c4a2e3e-9b7b-4a1e-8f39-2a3f6b7c9d10"
}
Failure (REQ_VALIDATION_FAILED specifically):
{
"success": false,
"message": "The given data was invalid.",
"error": {
"code": "REQ_VALIDATION_FAILED",
"fields": [
{"field": "did", "code": "REQ_FIELD_REQUIRED", "message": "The did field is required."}
]
},
"request_id": "9c4a2e3e-9b7b-4a1e-8f39-2a3f6b7c9d10"
}
Rate limits
- Two limits apply, and either can produce a SYS_RATE_LIMITED response with a Retry-After header:
- Per API key — your normal usage limit, applied once your key has been validated.
- Per source IP, for failed authentication only — a much lower limit that bounds invalid-key attempts. It does not apply once a request authenticates successfully.
Error codes
Codes marked Reserved are defined in the catalogue but not currently emitted by anything in this API.
AUTH_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
AUTH_KEY_MISSING |
401 | No API key was found on the request, in any of the four transports. | Check the key is actually being sent — see "Authentication". |
AUTH_KEY_INVALID |
401 | The key doesn't match any active key. Covers both "no such key" and "key exists but the secret is wrong" — these are deliberately indistinguishable from the outside. | Check the key was copied correctly; it's shown only once at creation, so a garbled copy may need a fresh key. |
AUTH_KEY_EXPIRED |
401 | The key has passed its expiry date, or has been revoked/regenerated. | Generate a new key from Administration → API Key. |
AUTH_KEY_DISABLED |
403 | The key exists and hasn't expired, but is currently disabled. | Re-enable it from Administration → API Key, or generate a new one. |
AUTH_IP_NOT_ALLOWED |
403 | The key has an IP allowlist configured, and the request's source IP isn't on it. | Add your server's IP to the key's allowlist, or clear the allowlist to permit all IPs. |
AUTH_MODULE_NOT_ALLOWED |
403 | The key is scoped to a specific module and this endpoint belongs to a different one. | Use a key scoped to "all", or the correct module, for this endpoint. |
AUTH_ACCOUNT_INACTIVE |
403 | The account that owns this key isn't currently active. | Contact your account administrator. |
REQ_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
REQ_VALIDATION_FAILED |
422 | One or more parameters failed validation. error.fields lists each failed field with its own code and message. | Fix the named field(s) and retry. |
REQ_NOT_FOUND |
— | 404: the URL doesn't match any endpoint in this API. 405: the URL matches an endpoint but the HTTP verb isn't GET/POST. Also the fallback for any unmatched/foreign route. | Check the URL and method against this document. |
REQ_NUMBERS_LIMIT_EXCEEDED |
422 | Voice Broadcast's numbers array exceeds the per-request cap (1,000 by default). | Split the recipient list into multiple requests. |
REQ_SCHEDULE_IN_PAST |
422 | schedule_at resolved to a time that's already passed by the moment the request was actually processed. | Resend with a later schedule_at, or omit it to send immediately. |
REQ_SCHEDULE_INVALID_FORMAT |
422 | schedule_at isn't a value the date parser accepts. | Use ISO-8601, with or without a UTC offset. |
REQ_AUDIO_SELECTION_INVALID |
422 | Neither or both of audio_uid/audio_reference were supplied where exactly one is required. | Supply exactly one of audio_uid, audio_reference. |
REQ_TARGET_SELECTION_INVALID (Reserved) |
— | Defined in the catalogue but not currently emitted anywhere in this API — no live endpoint has a target-selection ambiguity of this shape yet. | Not applicable today. |
REQ_FIELD_REQUIRED |
— | Field-level only — never a top-level error.code. Appears as one entry inside error.fields when that field was missing. | Supply the named field. |
REQ_FIELD_INVALID_FORMAT |
— | Field-level only. The default field-level code for any validation rule not covered by a more specific one. | Fix the named field's format. |
REQ_FIELD_OUT_OF_RANGE |
— | Field-level only. The field failed a min/max/between/size rule. | Adjust the named field's value/length within range. |
REQ_FIELD_UNKNOWN_VALUE |
— | Field-level only. The field's value isn't one of the accepted enumerated values. | Use one of the documented allowed values for that field. |
REQ_FIELD_NOT_PERMITTED |
— | Field-level only. The field was supplied but this endpoint doesn't accept it. | Remove the named field from your request. |
REQ_IDEMPOTENCY_KEY_CONFLICT |
409 | Voice Broadcast only. The Idempotency-Key header you sent was already used, but with a different request body than this one. | Use a fresh Idempotency-Key for a genuinely new request, or resend the exact original body if you meant to replay it. |
AUDIO_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
AUDIO_NOT_FOUND |
404 | audio_uid/reference doesn't match any of your audio assets. | Check audio_uid/reference against Audio Status. |
AUDIO_NOT_APPROVED |
422 | The audio asset hasn't been approved, or hasn't been delivered to the operator yet. | Poll Audio Status until status is approved, then retry. |
AUDIO_DISABLED |
422 | The audio asset has been turned off via Audio Enable/Disable. | Re-enable it, or select a different asset. |
BCAST_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
BCAST_NOT_FOUND |
404 | id/reference doesn't match any of your broadcasts. | Check the id/reference against the response from Voice Broadcast. |
BCAST_DID_INVALID |
422 | did doesn't match any of your account's active, voice.obd-enabled DIDs. | Check the number against your account's DID list. |
BCAST_NO_VALID_NUMBERS |
422 | Every recipient number submitted was invalid or a duplicate. | Check error.details.rejected_numbers for the reason per number, then retry with valid ones. |
BCAST_ALL_NUMBERS_INVALID |
422 | A more specific case of BCAST_NO_VALID_NUMBERS: every rejected number failed specifically because it could not be normalized to a valid number. | Reformat every number in the request and resend. |
BCAST_ALL_NUMBERS_DUPLICATE (Reserved) |
— | Provably unreachable today — numbers deduplication always keeps a number's first occurrence as valid, so a submission can never reach "all numbers rejected" with duplicate as the only reason. | Not applicable today. |
BILLING_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
BILLING_INSUFFICIENT_CREDITS |
402 | Your voice.obd wallet balance doesn't cover the estimated cost of the request. | Top up your voice.obd balance, or reduce the recipient count, then retry. |
BILLING_NO_ACTIVE_PLAN (Reserved) |
— | Every endpoint in this API today is wallet-billed; this code exists for a future plan-billed scope that isn't reachable from any published endpoint yet. | Not applicable today. |
BILLING_PLAN_QUOTA_EXHAUSTED (Reserved) |
— | Reserved, same reason as BILLING_NO_ACTIVE_PLAN. | Not applicable today. |
SYS_*
| Code | HTTP status | Meaning | What to do |
|---|---|---|---|
SYS_RATE_LIMITED |
429 | Too many requests. A Retry-After header on the response says how many seconds to wait. | Wait for the Retry-After duration, then retry. |
SYS_PROVIDER_ERROR |
502 | The call succeeded on our side but failed talking to the underlying voice provider/operator. | Safe to retry after a short delay. If it persists, contact support with the request_id. |
SYS_PROVIDER_TIMEOUT (Reserved) |
— | Reserved for a possible future endpoint that calls the operator synchronously in the request/response cycle; nothing in this API does that today. | Not applicable today. |
SYS_PROVIDER_REJECTED (Reserved) |
— | Reserved, same reason as SYS_PROVIDER_TIMEOUT. | Not applicable today. |
SYS_INTERNAL_ERROR |
500 | An unexpected error on our side. You will never see a stack trace or debug output here. | Contact support with the request_id. |