RFC 9457 problem+json
Error catalog
Every governed error code KYTRIX can return, what it means, and which ones are worth a branch in your integration.
Generated from ERROR_CATALOG in @kytrix/contracts · 38 codes
#The problem document
Every KYTRIX rejection is an RFC 9457 problem document served as application/problem+json. It always carries a code from the catalog below, and where a field is at fault, the dotted path to it.
{
"type": "kytrix:validation/pan_detected",
"title": "Payload contains a card-number (PAN) pattern",
"status": 400,
"code": "kytrix:validation/pan_detected",
"detail": "data.memo: must not contain a card number",
"instance": "/v1/movements",
"errors": [
{
"path": "data.memo",
"code": "kytrix:validation/pan_detected",
"message": "must not contain a card number"
}
],
"correlation_id": "6f9f0f2e-6b0e-4a1d-9d2f-2f6c3f2a1b77"
}| Member | Meaning |
|---|---|
code | The governed catalog value. This is what you branch on. |
type | The same value again, so a generic RFC 9457 client can key on either member. |
title | Short, stable, human-readable summary of the code. |
status | The HTTP status. Also on the response line. |
detail | A human explanation naming the fix. Free text — never parse it. |
instance | The request path that produced the problem. |
errors[] | Field-level breakdown: path (dotted, array indices numeric), code, message. An empty path means the whole body. |
correlation_id | Quote this to KYTRIX support. Send your own with Kytrix-Correlation-Id and it travels into the logs. |
import { ErrorCode, isKytrixApiError } from '@kytrix/sdk';
try {
await kytrix.movements.submit(event, { idempotencyKey: row.id });
} catch (err) {
if (!isKytrixApiError(err)) throw err;
switch (err.code) {
case ErrorCode.ValidationPanDetected: // a card number reached a free-text field
return quarantineRow(row, err.errorsAt('data.memo'));
case ErrorCode.SequenceConflict: // same entity version, different content
return bumpSequenceAndRetry(row);
case ErrorCode.IdempotencyKeyConflict: // this key was used with a different body
return alertOncall('idempotency key reused', err.correlationId);
case ErrorCode.RateLimitExceeded: // the SDK already backed off maxAttempts times
return deferRow(row, err.rateLimit?.retryAfterSeconds ?? 60);
default:
throw err;
}
}#What is retryable
| Status | Retry? | Why |
|---|---|---|
400 / 422 | Never | The payload cannot become valid by being sent again. Fix the mapping. |
401 | Only after fixing | A bad signature, a stale clock or a reused nonce. Re-signing with a fresh nonce and a correct clock is a new request, not a retry. |
403 | Never | The credential lacks the scope. Issue a key that has it. |
409 | Never blindly | sequence/conflict means your content differs — decide what the right version is. idempotency/key_conflict means you reused a key; pick a new one or resend the original body. |
413 | Never | Split the batch or the body. |
429 | Yes | Honour Retry-After; back off with jitter. |
500 / 502 / 503 / 504 | Yes, with a key | Retry only writes that carried an Idempotency-Key. An unkeyed POST that timed out must not be repeated — a duplicated movement is worse than a failure you can see. |
| no response at all | Yes, with a key | Same rule. The SDK enforces it for you. |
#The catalog
All 38 codes, generated from ERROR_CATALOG in packages/contracts/src/errors.ts. Codes are grouped by family; the family is the segment between kytrix: and /.
#auth/*
The request signature, the timestamp, the nonce or the credential is the problem.
| Code | Status | Title |
|---|---|---|
kytrix:auth/invalid_signature | 401 | Request signature is invalid |
kytrix:auth/timestamp_skew | 401 | Request timestamp is outside the allowed clock window |
kytrix:auth/nonce_replayed | 401 | Request nonce was already used |
kytrix:auth/unknown_key | 401 | Unknown API key id |
kytrix:auth/key_revoked | 401 | API key has been revoked |
kytrix:auth/key_expired | 401 | API key has expired |
kytrix:auth/insufficient_scope | 403 | Credential lacks the scope required for this operation |
kytrix:auth/unauthenticated | 401 | Authentication required |
#authz/*
Authenticated, but not allowed to do this.
| Code | Status | Title |
|---|---|---|
kytrix:authz/forbidden | 403 | Forbidden |
#validation/*
The payload itself is unacceptable. Fix the mapping — a retry cannot help.
| Code | Status | Title |
|---|---|---|
kytrix:validation/schema | 400 | Request does not match the schema |
kytrix:validation/pan_detected | 400 | Payload contains a card-number (PAN) pattern |
kytrix:validation/money_scale_mismatch | 400 | Money scale does not match the asset registry |
kytrix:validation/timestamp_out_of_range | 400 | Timestamp is outside the accepted range |
kytrix:validation/unsupported_schema_version | 400 | Unsupported schema_version |
#sequence/*
A version conflict on an identity you already sent. Never a silent overwrite.
| Code | Status | Title |
|---|---|---|
kytrix:sequence/conflict | 409 | Same identity with different content and no higher sequence |
kytrix:sequence/stale | 409 | Sequence is lower than an already-applied version |
#idempotency/*
Your Idempotency-Key was used differently, or is still in flight.
| Code | Status | Title |
|---|---|---|
kytrix:idempotency/key_conflict | 409 | Idempotency-Key was already used with a different body |
kytrix:idempotency/in_flight | 409 | A request with this Idempotency-Key is still being processed |
#batch/*
The batch envelope broke a limit.
| Code | Status | Title |
|---|---|---|
kytrix:batch/too_large | 413 | Batch exceeds the maximum number of events |
#reference/*
Well formed, but it points at a canonical object KYTRIX does not know. Send the referenced object first.
| Code | Status | Title |
|---|---|---|
kytrix:reference/unknown_party | 422 | Referenced party is unknown |
kytrix:reference/unknown_account | 422 | Referenced account is unknown |
kytrix:reference/unknown_instrument | 422 | Referenced instrument is unknown |
kytrix:reference/unknown_movement | 422 | Referenced movement is unknown |
#rate_limit/*
You are over the per-tenant budget. Honour Retry-After.
| Code | Status | Title |
|---|---|---|
kytrix:rate_limit/exceeded | 429 | Rate limit exceeded |
#request/*
A transport-level limit.
| Code | Status | Title |
|---|---|---|
kytrix:request/payload_too_large | 413 | Request payload too large |
#not_found/*
No such object in this tenant. Cross-tenant reads do not exist to be found.
| Code | Status | Title |
|---|---|---|
kytrix:not_found/resource | 404 | Resource not found |
#conflict/*
A resource-state conflict with no more specific family.
| Code | Status | Title |
|---|---|---|
kytrix:conflict/resource | 409 | Resource conflict |
#webhook/*
The webhook endpoint URL is not one KYTRIX will deliver to.
| Code | Status | Title |
|---|---|---|
kytrix:webhook/invalid_endpoint | 400 | Webhook endpoint URL is not allowed |
#import/*
Something about a historical import or one of its chunks.
| Code | Status | Title |
|---|---|---|
kytrix:import/invalid_chunk | 400 | Import chunk is invalid |
kytrix:import/not_found | 404 | Import not found |
#tenant/*
The tenant behind the credential is unknown or suspended.
| Code | Status | Title |
|---|---|---|
kytrix:tenant/unknown | 401 | Unknown tenant |
kytrix:tenant/suspended | 403 | Tenant is suspended |
#governance/*
A governed workflow refused: approval missing, maker equals checker, an immutable object, or an illegal state transition. Mostly console surfaces.
| Code | Status | Title |
|---|---|---|
kytrix:governance/approval_required | 409 | Operation requires an approval that is not present |
kytrix:governance/maker_checker_violation | 403 | Approver must differ from the maker |
kytrix:governance/immutable | 409 | Object is immutable |
kytrix:governance/invalid_transition | 409 | State transition is not allowed |
#service/*
KYTRIX is shedding load or degraded. Retry with backoff.
| Code | Status | Title |
|---|---|---|
kytrix:service/unavailable | 503 | Service temporarily unavailable |
#internal/*
A KYTRIX bug. Quote correlation_id to support.
| Code | Status | Title |
|---|---|---|
kytrix:internal/error | 500 | Internal error |
#The ones you will actually meet
Most of the catalog belongs to console workflows you will never call. On the ingestion path, these are the codes worth writing a branch for on day one:
kytrix:validation/schema— the payload does not match the contract.errors[].pathnames the field. This is a mapping bug.kytrix:validation/pan_detected— a Luhn-valid 13–19 digit run reached a free-text field. Send a token or first6/last4 at most; never the number.kytrix:validation/money_scale_mismatch— an amount carries more precision than its asset scale allows. KYTRIX will not round your money for you.kytrix:validation/unsupported_schema_version— the envelope pins aschema_versionthis build does not accept.kytrix:sequence/conflict— you resent an identity with different content and no highersequence. For movements and control events, corrections are new events withlinks.correction_of.kytrix:idempotency/key_conflict— the sameIdempotency-Keywith a different body. Almost always a key-derivation bug on your side.kytrix:auth/timestamp_skew— your clock is more than 5 minutes off ours. Run NTP.kytrix:auth/nonce_replayed— you re-sent captured headers. Generate a fresh nonce per attempt, including per retry.kytrix:rate_limit/exceeded— over the per-tenant token bucket.Retry-Aftersays how long to wait.