Error Codes and Response Shape
Every Macha API error returns a stable code your code can switch on. Reference for every code we emit.
Every Macha API error follows the same shape, with a stable code string you can switch on. We never repurpose codes, if a new error condition arises, we give it a new code rather than overload an existing one.
Error response shape
{
"error": {
"code": "agent_handle_taken",
"message": "Agent with handle @ticketTriage already exists",
"request_id": "req_a0b88aa084bac0f7"
}
}
Field semantics
| Field | Stability | Use for |
|---|---|---|
code | Stable. Never changes between releases without a v-bump. | Branching logic. Switch on this to decide what to do. |
message | Human-friendly. May be reworded between releases. | Logging, surfacing to end users. Never machine-parse. |
request_id | Per-request. Also returned in the X-Request-ID header. | Log it. Macha support needs it to trace anything. |
HTTP status semantics
Macha follows RFC 7231 strictly. We don't return 200 on errors. The HTTP status alone tells you the rough category; the code tells you the exact reason.
| Status | Category | Should you retry? |
|---|---|---|
400 | Bad request | No, fix the request. |
401 | Authentication failed | No, check the key. |
403 | Authorization failed (scope or policy) | No, the key needs different scopes. |
404 | Resource doesn't exist | No, check the ID. |
409 | State conflict (uniqueness, idempotency reuse) | Sometimes, depends on the code. See below. |
422 | Validation failed | No, the input is invalid. |
429 | Rate limited | Yes, honor Retry-After. |
500 | Server error | Yes, with exponential backoff. Use an Idempotency-Key. |
503 | Service unavailable | Yes, with exponential backoff. |
Error code registry
These are the codes Macha v1 currently emits. Adding new codes is non-breaking (additive). Removing or renaming a code is breaking and would require v2.
Authentication & authorization
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, malformed, revoked, or unknown API key. Verify the Authorization: Bearer ... header. |
insufficient_scope | 403 | Valid key, lacks the required scope for this endpoint. |
forbidden | 403 | Org-level block, e.g. deletion-scheduled org, suspended subscription. |
Validation & not-found
| Code | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Malformed input that isn't a domain-validation issue (e.g. invalid JSON). |
validation_failed | 422 | Required field missing, value out of range, enum mismatch, etc. message names the offending field. |
not_found | 404 | Generic. Used for unknown nested resources. |
agent_not_found | 404 | Agent ID doesn't exist (or has been soft-deleted past the trash window). |
conversation_not_found | 404 | Conversation ID unknown or belongs to another org. |
custom_tool_not_found | 404 | Custom tool deleted or never existed. |
source_not_found | 404 | Source deleted or never existed. |
Conflicts
| Code | HTTP | Meaning |
|---|---|---|
agent_handle_taken | 409 | Trying to create or rename an agent to a handle that already exists in the org (case-insensitive). |
custom_tool_name_taken | 409 | Custom tool with this generated name already exists. |
upload_source_exists | 409 | POST /sources with type=upload, the Uploads source is a per-org singleton and is already provisioned. |
idempotency_key_reused | 409 | Same Idempotency-Key seen within 24h with a different request body. Generate a fresh key for a different operation. |
plan_limit_exceeded | 422 | The action would exceed your plan's resource cap. Returned at write time, never read time. |
Throttling & server
| Code | HTTP | Meaning |
|---|---|---|
rate_limited | 429 | Per-key rate limit exceeded. Inspect Retry-After and X-RateLimit-* headers. |
internal_error | 500 | Unhandled server exception. Log the request_id, if it persists, contact support. |
service_unavailable | 503 | Provider unreachable, queue full, or planned maintenance. Retry with backoff. |
Retry guidance
The boring rule: retry 5xx and 429, never retry 4xx.
Slightly less boring rule: retries on write endpoints should always carry an Idempotency-Key header. Otherwise you may end up with duplicate resources if the original request actually succeeded but the response got lost on the wire.
Recommended retry pattern
async function callMacha(path, init) {
const url = `https://dashboard.getmacha.com/api/v1${path}`;
const headers = {
'Authorization': `Bearer ${process.env.MACHA_API_KEY}`,
...(init?.headers || {}),
};
let lastError;
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url, { ...init, headers });
if (res.status < 500 && res.status !== 429) return res; // success or non-retryable
// 429 honors Retry-After; 5xx uses exponential backoff with jitter.
const retryAfter = parseFloat(res.headers.get('Retry-After'));
const backoff = Number.isFinite(retryAfter)
? retryAfter * 1000
: Math.min(1000 * 2 ** attempt, 30_000) + Math.random() * 250;
await new Promise(r => setTimeout(r, backoff));
lastError = res;
}
return lastError; // give up after 5 tries
}
If the original POST actually succeeded but the response was lost, a naive retry creates a duplicate. With Idempotency-Key set, Macha returns the cached original response on the retry, no duplicate. See Idempotency.
Logging for support
When something goes wrong and you need Macha to look at it, capture:
- The request URL (with query string)
- The HTTP method
- The response status code
- The response
error.code - The
request_id(from response body orX-Request-IDheader) - The timestamp
That's enough for us to trace the exact request through our logs in seconds. Without the request_id, support takes minutes-to-hours.
© 2026 AGZ Technologies Private Limited