Errors
Every error response has the same shape:
{
"statusCode": 400,
"error": "Bad Request",
"message": "name must be a string"
}
message is a string for most errors, but an array of strings for
validation errors that fail on more than one field at once:
{
"statusCode": 400,
"error": "Bad Request",
"message": ["name must be a string", "firstMessage must be a string"]
}
Always handle both shapes — check whether message is an array before
treating it as a single string, or just .join(', ') it either way.
Status codes you'll actually see
| Code | Meaning | Typical cause |
|---|---|---|
400 | Bad Request | A required field is missing, or fails validation |
401 | Unauthorized | Missing, invalid, expired, or revoked credentials |
404 | Not Found | The id doesn't exist — or belongs to another org |
429 | Too Many Requests | You've hit a rate limit |
Why cross-tenant access looks like 404, not 403
If you request a resource (an agent, a call, a campaign) that belongs to a
different organization, you get 404 Not Found — the same response as if
the id genuinely didn't exist anywhere. This is deliberate: a 403 Forbidden would confirm "this id exists, you're just not allowed to see
it," which leaks information about other tenants' data. 404 reveals
nothing either way.
If you're debugging a mysterious 404 for an id you're sure is correct, double-check you're using the API key (or session) for the organization that actually owns it.
Example: handling errors in a client
const res = await fetch('https://convostack.ai/api/agents', {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
const body = await res.json();
const message = Array.isArray(body.message) ? body.message.join(', ') : body.message;
throw new Error(`ConvoStack API error (${res.status}): ${message}`);
}
const agents = await res.json();