Status codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | Success, no body |
| 400 | Malformed request (rare — most validation uses 422) |
| 401 | Missing or bad API key |
| 403 | Key lacks the required scope (future) |
| 404 | Resource doesn’t exist OR belongs to another tenant |
| 409 | State conflict (e.g. /predictive/pause on a non-running campaign) |
| 422 | Validation error — body failed a Pydantic constraint |
| 429 | Rate-limited |
| 500 | Our bug. Email support with the request timestamp. |
| 502/503/504 | Upstream momentarily unavailable. Retry with backoff. |
Tenant-scoped 404s
When you GET a resource that exists but belongs to another tenant, you get a 404, not a 403. We never want to confirm existence across tenant boundaries (otherwise an attacker could enumerate other customers’ campaign IDs).Validation errors you’ll see often
predictive_target_abandon > 0.030
Inverted ratio range
max >= min. PATCHing just one is always allowed (we defer
to the DB CHECK).
Invalid phone
Abandon rate exceeded (business-level)
Not an HTTP error — it’s a lifecycle event. When a predictive campaign’s abandon rate crosses its target for 60s sustained, the backend auto-pauses the campaign. You’ll see:- The campaign’s
statusflip topaused - A
campaign.pausedwebhook withdata.source = "abandon_rate_exceeded"
POST /api/campaigns/{id}/predictive/resume (dashboard
JWT auth, not the v1 API key). See the operator runbook in your
dashboard for guidance.