The v2 REST API uses a single, predictable error envelope across every endpoint. Whatever the failure — a missing auth token, a Zod validation problem, a 404, a downstream rate limit — the body shape is the same. Build your error-handling once and reuse it everywhere.
This page covers v2 REST endpoints. MCP tool errors follow the ADCP error
spec and
are returned in structuredContent rather than HTTP status codes.
Error envelope
Every non-success response has data: null and a populated error object:
{
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"field": "name",
"details": {
"issues": [
{ "path": "name", "message": "Required" }
]
}
}
}
| Field | Type | Always present | Description |
|---|
code | string | yes | Machine-readable error code (see table below) |
message | string | yes | Human-readable message safe to surface to end users |
field | string | no | Field path for validation errors (e.g. start_date, targeting.geos) |
details | object | no | Structured payload — Zod issues, conflict resource ids, retry hints, etc |
Successful responses have the inverse shape: { "data": <result>, "error": null }. List endpoints add a meta block (see Pagination).
HTTP status codes
| Status | Meaning | Typical code values |
|---|
400 | Malformed request | BAD_REQUEST, VALIDATION_ERROR, CURRENCY_MISMATCH |
401 | Missing or invalid auth | UNAUTHORIZED |
403 | Authenticated but not allowed | FORBIDDEN, ACCESS_DENIED, ALPHA_OPT_IN_REQUIRED, TOS_ACCEPTANCE_REQUIRED |
404 | Resource doesn’t exist or isn’t visible to you | NOT_FOUND |
409 | Conflicting state (duplicate, wrong state, budget short) | CONFLICT, PRICING_NOT_CONFIGURED, INSUFFICIENT_MEDIA_BUDGET, ROUTED_AGENT_REQUIRES_OPERATOR_AUTH |
422 | Semantically invalid (rare — most things use 400) | VALIDATION_ERROR |
429 | Rate limit hit | RATE_LIMITED (see Rate Limits) |
500 | Unhandled server error | INTERNAL_ERROR |
501 | Endpoint exists but not yet implemented | NOT_IMPLEMENTED |
503 | Upstream / dependency unavailable | SERVICE_UNAVAILABLE |
Common error codes
| Code | When you’ll see it |
|---|
VALIDATION_ERROR | Zod schema rejected the request body, query, or params |
BAD_REQUEST | Generic 400 — usually a malformed param that didn’t reach Zod |
UNAUTHORIZED | No bearer token, expired token, or unknown API key |
FORBIDDEN | Auth succeeded but you lack the role/permission |
ACCESS_DENIED | Resource exists but is owned by a different customer/seat |
NOT_FOUND | Resource ID doesn’t exist (or is hidden from your scope) |
CONFLICT | Duplicate resource, illegal state transition, or invariant violated |
RATE_LIMITED | Too many requests — back off and retry after Retry-After |
INTERNAL_ERROR | Unhandled exception — safe to retry once |
SERVICE_UNAVAILABLE | A dependency (downstream agent, billing, signal provider) is temporarily down |
Domain-specific codes you may encounter on campaign endpoints:
| Code | Meaning |
|---|
PRICING_NOT_CONFIGURED | Advertiser has no pricing rule for the selected sales agent |
CURRENCY_MISMATCH | Budget currency doesn’t match the agent or storefront currency |
INSUFFICIENT_MEDIA_BUDGET | Requested budget is below the configured floor |
ROUTED_AGENT_REQUIRES_OPERATOR_AUTH | Routed sales agent needs operator credentials configured first |
ALPHA_OPT_IN_REQUIRED | Feature is in alpha — opt in via support before using |
TOS_ACCEPTANCE_REQUIRED | Account must accept the latest terms of service before mutating |
Validation errors
When request validation fails, code is VALIDATION_ERROR and details.issues enumerates every problem Zod found, with dotted field paths:
{
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"issues": [
{ "path": "name", "message": "Required" },
{ "path": "budget.amount", "message": "Number must be positive" },
{ "path": "flightDates.start", "message": "Invalid date" }
]
}
}
}
When a single-field check fails (e.g. a route guard), field is set instead:
{
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Start date must be in the future",
"field": "start_date"
}
}
Always render details.issues[].path in your UI — the user usually just needs
to know which form field to fix.
Handling errors in client code
curl -i https://api.agentic.scope3.com/api/buyer/campaigns \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{"advertiserId": 123}'
# HTTP/1.1 400 Bad Request
# Content-Type: application/json
#
# {
# "data": null,
# "error": {
# "code": "VALIDATION_ERROR",
# "message": "Request validation failed",
# "details": {
# "issues": [
# { "path": "name", "message": "Required" },
# { "path": "budget", "message": "Required" }
# ]
# }
# }
# }
Don’t pattern-match on message text — message strings may be reworded for
clarity. Always branch on error.code (and on HTTP status as a fallback).
Retrying safely
RATE_LIMITED, INTERNAL_ERROR, and SERVICE_UNAVAILABLE are transient — retry GETs with exponential backoff. For creation/mutation requests after a 5xx, prefer to surface the error rather than auto-retry, since duplicate-create protection isn’t enforced server-side.
VALIDATION_ERROR, NOT_FOUND, FORBIDDEN, ACCESS_DENIED, and CONFLICT are terminal — don’t retry until the input or state changes.