Errors
One envelope, typed codes, request ids that round-trip into the audit log.
Every error response from GenieOS uses the same envelope:
{
"error": {
"type": "validation_error", // category — one of the values below
"code": "schema_contract_violation", // specific code, scoped to type
"message": "Variable `plan` must be one of [\"hobby\", \"pro\"], got \"founders\".",
"fields": [ // present on validation errors
{ "path": "variables.plan", "code": "enum_violation" }
],
"retry_after_seconds": null, // present on rate_limit_exceeded
"request_id": "req_01JABC..." // round-trips into the audit log
}
}The status code matches the type:
| Type | Status | When it happens |
|---|---|---|
authentication_error | 401 | Missing / malformed / revoked key |
permission_denied | 403 | Key valid; scope insufficient |
not_found | 404 | Resource doesn\u2019t exist (or is in another workspace) |
validation_error | 422 | Body is wrong shape / fails the schema contract |
conflict | 409 | Idempotency mismatch / version race |
rate_limit_exceeded | 429 | Per-key or per-workspace; see Rate limits |
internal_error | 500 | Bug on our side. Retry; we\u2019ll page |
bad_gateway | 502 | Upstream connector hiccup. Retry |
service_unavailable | 503 | Maintenance / capacity. Retry |
gateway_timeout | 504 | Upstream connector timeout. Retry |
The codes underneath are stable. We grow the code set with new values rather than renaming or repurposing existing ones, so your error-handling switch is forward-compatible.
Common codes
authentication_error
missing_authorization_headerinvalid_api_keyrevoked_api_key
permission_denied
scope_requiredworkspace_mismatchfeature_not_enabled
validation_error
body_invalid_jsonfield_requiredfield_type_mismatchschema_contract_violationidempotency_key_invalidunknown_template
conflict
idempotency_key_mismatchidempotency_key_in_progresstemplate_version_mismatchwebhook_signing_secret_in_use
rate_limit_exceeded
key_rate_limitworkspace_rate_limitconcurrent_writes_exceeded
SDK error classes
Both SDKs unwrap the envelope into typed exception classes — no
string-matching on message.
import {
GenieOS,
GenieOSValidationError,
GenieOSRateLimitError,
} from '@genie-os/sdk';
try {
await mg.templates.send({ to, template: 'welcome', variables });
} catch (err) {
if (err instanceof GenieOSValidationError) {
for (const f of err.fields) console.warn(f.path, f.code);
return;
}
if (err instanceof GenieOSRateLimitError) {
await sleep(err.retryAfterSeconds * 1000);
return retry();
}
throw err;
}from genieos import (
GenieOS,
GenieOSValidationError,
GenieOSRateLimitError,
)
try:
mg.templates.send(to=to, template="welcome", variables=variables)
except GenieOSValidationError as e:
for f in e.fields:
print(f.path, f.code)
except GenieOSRateLimitError as e:
sleep(e.retry_after_seconds)The full class hierarchy:
GenieOSError— base.GenieOSAuthError— 401 / 403.GenieOSNotFoundError— 404.GenieOSValidationError— 422 / 409.GenieOSRateLimitError— 429.GenieOSServerError— 5xx.GenieOSNetworkError— DNS / TCP / TLS / timeout.
request_id
Every response — success or error — carries MailGenius-Request-Id as
both a header and (on errors) inside the envelope. Quote it whenever you
open a support ticket; it round-trips into the audit log so we can find
the exact write you saw.
curl -i ... | grep -i mailgenius-request-id
# MailGenius-Request-Id: req_01JABCXYZQ9V...What about field-level validation?
The fields[] array on validation_error contains one entry per offending
input. Codes you\u2019ll see most often:
| Code | Meaning |
|---|---|
missing | Required field absent |
type_mismatch | Wrong JSON type (e.g. string given for integer) |
enum_violation | Value not in the declared enum |
format_invalid | String didn\u2019t match a declared format (email, url, uuid) |
length_out_of_range | String / array length outside declared bounds |
unknown_field | Field present but not declared in the schema (strict mode only) |
This means your form code can do something better than "Something went wrong." See Schema contract for how the contract generates these codes.
Always log request_id
When something goes sideways at 3am, the difference between "my
webhook delivered weird stuff" and "request req_01JABCXYZ... shows
the upstream Stripe payload arrived malformed" is whether you logged
the request id. Always log it.