Errors
Every error has the same JSON envelope, a stable machine code and a request id. Here is every code, its HTTP status and what to do about it.
A failed call answers with a 4xx or 5xx status and this body:
{
"error": {
"code": "validation_failed",
"message": "limit Too big: expected number to be <=100",
"details": [
{ "field": "limit", "message": "Too big: expected number to be <=100", "code": "too_big" }
],
"requestId": "19d815e9-2261-4a45-a3f2-0333e120eea5"
}
}| Field | What it is |
|---|---|
code | A stable snake_case code. Branch on this, never on message. |
message | A human-readable explanation. It can change wording at any time. |
details | For validation_failed, a list of { field, message, code }, one per invalid field. Otherwise extra context as an object, or null. |
requestId | The same value as the Koast-Request-Id response header. Quote it to support. |
Every response, successful or not, carries the Koast-Request-Id header.
Codes
| HTTP | code | Meaning | What to do |
|---|---|---|---|
| 400 | validation_failed | A parameter or body field is missing, has the wrong type, is out of range, or isn't a known field. Also an invalid or expired cursor. | Fix the request using details. Don't retry unchanged. |
| 400 | idempotency_key_required | launch-campaign or edit-live-campaign was sent without an Idempotency-Key header. | Add one. See Idempotency. |
| 401 | unauthenticated | No credential, or one Koast doesn't recognise, or a revoked or expired key. | Check the Authorization header and the key. |
| 401 | audience_mismatch | An OAuth token was sent to a host or path outside the API, or the token was issued for the MCP server, not the API. | Call https://api.koast.ai/v1 with a token issued for it, for example after koast login. |
| 403 | plan_required | The organization isn't on an active Agency plan. | See Access and plans. |
| 403 | surface_disabled | The key has API access turned off. | An owner or admin turns on API for the key. |
| 403 | insufficient_scope | The key lacks the scope this operation needs. details.requiredScope names it. | Use a key with that scope. |
| 403 | brand_not_allowed | The key is limited to other brands, or the OAuth user's role gives them only some brands. | Use a credential that covers this brand. |
| 403 | forbidden | The credential can't reach this brand or resource, or its role doesn't allow the action. | Check the brand id and the user's role. |
| 403 | spend_ceiling_required | A publishing call from a key with no spend ceiling. | Set a publish limit on the key. |
| 403 | spend_ceiling_exceeded | The call would take the key past its spend ceiling. Nothing was published. | Raise the limit or use another key. |
| 404 | not_found | No such endpoint, or no such resource in this brand. | Check the path and ids. |
| 409 | idempotency_conflict | The Idempotency-Key was already used with a different body, or the first request with it is still running. | Use a new key for a new request, or wait and retry the same request. |
| 409 | stale_read | The resource changed since you read it. | Read it again, then retry. |
| 409 | conflict | The request conflicts with the resource's current state, such as a test send to a disabled webhook endpoint ("Enable the endpoint first."). | Read the message, fix, retry. |
| 413 | validation_failed | The request body is larger than 5 MB. | Send a smaller body. |
| 422 | capability_unsupported | The ad platform behind this brand doesn't support this operation or setting. | Don't retry. |
| 422 | account_ambiguous | The brand has more than one ad account and the call didn't say which. | Pass the accountId. |
| 422 | unprocessable | The request is valid but can't be carried out, like a 21st webhook endpoint. | Read the message. |
| 429 | rate_limited | Too many requests or too many in flight for your organization. | Wait Retry-After seconds. See Rate limits. |
| 500 | internal_error | Something failed on Koast's side. | Retry with backoff. Contact support with the requestId if it persists. |
| 503 | unavailable | A dependency couldn't answer, for example the spend ceiling couldn't be checked. Nothing was published. | Retry shortly. |
| 503 | webhooks_unavailable | Webhooks aren't available in this environment. | Contact support. |
| 504 | timeout | The request didn't finish within 30 seconds. It may still complete. | Retry with the same Idempotency-Key. |
New codes may be added within v1. Treat an unknown code by its HTTP status. See Versioning.
Examples
A field the endpoint doesn't know. Every endpoint rejects unknown query and body fields:
curl "https://api.koast.ai/v1/brands/1/launches?colour=blue" \
-H "Authorization: Bearer $KOAST_API_KEY"{
"error": {
"code": "validation_failed",
"message": "colour is not a recognised field",
"details": [
{ "field": "colour", "message": "is not a recognised field", "code": "unrecognized_key" }
],
"requestId": "7afaef82-028e-4156-8a15-8d79211fc6ab"
}
}An endpoint that doesn't exist:
curl https://api.koast.ai/v1/campaigns \
-H "Authorization: Bearer $KOAST_API_KEY"{
"error": {
"code": "not_found",
"message": "No endpoint matches GET /v1/campaigns.",
"details": null,
"requestId": "6e85b1a4-0f16-4162-b662-5f7dc6269cf5"
}
}A publish without an Idempotency-Key:
curl -X POST https://api.koast.ai/v1/brands/1/launches/cmv09p0ny000bvcdsyaeedbj7/publish \
-H "Authorization: Bearer $KOAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"error": {
"code": "idempotency_key_required",
"message": "POST /v1/brands/{brandId}/launches/{launchId}/publish requires an Idempotency-Key header.",
"details": null,
"requestId": "c0fdb4c6-9d6b-44f7-a838-2cee90c46318"
}
}Several invalid fields at once. details lists each one:
{
"error": {
"code": "validation_failed",
"message": "level Invalid option: expected one of \"CAMPAIGN\"|\"ADSET\" (and 2 more)",
"details": [
{ "field": "level", "message": "Invalid option: expected one of \"CAMPAIGN\"|\"ADSET\"", "code": "invalid_value" },
{ "field": "budgetType", "message": "Invalid option: expected one of \"daily\"|\"lifetime\"", "code": "invalid_value" },
{ "field": "amount", "message": "Invalid input: expected number, received undefined", "code": "invalid_type" }
],
"requestId": "37b819fd-86b7-43ce-bea2-2af0de0a8823"
}
}A launch that fails validation, on POST /v1/brands/{brandId}/launches/{launchId}/publish with or without "confirm": true, gets 400 validation_failed. message counts the issues and details has one { field, message } per issue. field takes the form campaigns[<nodeId>].<setting>, adSets[<nodeId>].<setting> or ads[<nodeId>].<setting>, where <setting> is a dotted path such as budget.dailyAmount:
{
"error": {
"code": "validation_failed",
"message": "Launch validation failed: 2 issues.",
"details": [
{ "field": "adSets[as1].budget.dailyAmount", "message": "Daily budget must be at least $1.00 (100 cents)" },
{ "field": "ads[ad1].creativeId", "message": "A creative (image or video) is required" }
],
"requestId": "5a0c7e21-3d4f-4b8a-9e61-0f2b7d9c1a34"
}
}Malformed JSON
A body that isn't valid JSON gets the same envelope, with 400 validation_failed:
{
"error": {
"code": "validation_failed",
"message": "The request body is not valid JSON.",
"details": null,
"requestId": "8f1e714f-d55b-4adf-a6d1-cd7f70aa95ae"
}
}Send valid JSON with Content-Type: application/json.