Skip to content
PacSpace
Talk to us

Errors

The answer envelope, the codes a records integration meets, what each one means, what to retry, and how each SDK hands a refusal to your code.

Every answer from the records API has one shape. A refusal carries a sentence that states the rule and, in almost every case, a code from the API's registry, in screaming snake case. Branch on the code, never on the sentence.

The envelope

A success:

json
{ "success": true, "data": { "...": "the answer" }, "message": "optional" }

A refusal:

json
{
  "success": false,
  "error": {
    "statusCode": 422,
    "code": "RECORD_TITLE_TOO_LONG",
    "message": "A title fits in 120 characters.",
    "timestamp": "2026-10-05T14:09:48.102Z",
    "path": "/api/v1/records/machine-action-record/eval-run-4417/transitions"
  }
}

Every answer that names a wait, a 202 IDEMPOTENCY_PENDING and every 429, carries retryAfterSeconds in error and the same number in a Retry-After header. The history route and the disclosure-source route return their files as the body, without the envelope.

Two refusals carry no code: a missing, unknown, or disabled key answers 401, and a body the API cannot read (a field missing or of the wrong type, or a field the route does not take) answers 400 with the validator's sentence.

A sentence that would name a key or a token reaches you as "An error occurred while processing your request.", with its code unchanged. That is one more reason to branch on the code.

Codes

Keys and workspaces

CodeStatusThe ruleWhat to do
none401A key is one your workspace lists under Settings, Developer, API keys, sent as x-api-key.Check that the key is set and has not been disabled.
RECORDS_API_NOT_ENABLED403Records are written from a records workspace.Pick the records option at signup, or ask us to switch the workspace.
RECORDS_CONTRACT_NOT_READY412A workspace writes once its recording environment is ready. Nothing was queued.Wait for the dashboard to show ready, then send again.
PLAN_LIMIT_REACHED402A Free workspace writes 100 entries a month. "A Free workspace writes 100 entries a month. Nothing was queued. Move to a paid plan to continue."Move to a paid plan, then send again with the same idempotency key.
PAID_ENTITLEMENT_REQUIRED402A paid plan writes while its payment stands.Settle the subscription on the dashboard's Plan page, then send again with the same key.
RATE_LIMIT_EXCEEDED429A workspace writes up to 120 entries a minute, shares up to 30 links a minute, and reads under a default budget.Wait retryAfterSeconds; send a write again with the same idempotency key. The SDKs do this themselves.

Writing an entry

CodeStatusThe ruleWhat to do
RECORD_LAYOUT_UNKNOWN400A record type is one of the published names.Use machine-action-record, or leave recordType out.
RECORD_LAYOUT_NOT_ADMITTED422The record type is published but not yet open for writes in this environment. Nothing was queued.Ask us when it opens here.
RECORD_LAYOUT_MISMATCH409A record keeps the record type it started with.Use a new record id for the new record type.
RECORD_STATE_UNKNOWN400A state comes from the record type's declared list.Use recorded, amended, or closed.
RECORD_CONTENT_INVALID400The sealed record carries the fields the record type names, each in its declared form.Fix the field the message names and send again.
RECORD_PAYLOAD_ALG_UNSUPPORTED400A fingerprint is made with sha-256 or hmac-sha-256.Fingerprint the file with the SDK's helper and send again.
RECORD_BLINDING_INCLUDED400"This write includes a blinding file or its value. Send only the fingerprint's alg, digest and byteLength; the blinding file stays with you."Send the fingerprint's ref as the payload, and keep the blinding file beside the file.
RECORD_PAYLOADS_REQUIRED422"Every record carries at least one fingerprint." Each entry needs one.Fingerprint the file or the payload and put the result in payloads.
RECORD_TITLE_TOO_LONG422"A title fits in 120 characters."Shorten the title; the rest belongs in description.
RECORD_REFERENCE_NOT_FOUND422"A reference names an entry that is already committed in your workspace."Check the record id and entry number, or wait for that entry to reach committed.
RECORD_AMENDS_NOT_FOUND422"An amendment names a committed entry in the same record, or in a closed record this entry references." Nothing was queued.Check the record id and the entry number.
RECORD_AMENDS_NOT_PRECEDING422"An amendment comes after the entry it amends." Nothing was queued.Wait for that entry to commit, or name the entry you meant.
RECORD_INTAKE_FAILED500A write that did not land is always reported.Send the same request again with the same idempotency key.
IDEMPOTENCY_PENDING202One idempotency key is one write; a repeat waits for the first to finish.Wait retryAfterSeconds, then send the same request again. The SDKs return this rather than throwing.
IDEMPOTENCY_CONFLICT409One idempotency key is one write; a different body under the same key is a different write.Send the new entry with a new key.

The three amendment and reference refusals add a second sentence naming the entry, such as which entry of the record is the latest committed.

After the write was queued

These reach you as record.failed at your webhook and as status: failed with the code in the history, never in the write's own answer.

CodeThe ruleWhat to do
RECORD_GENESIS_MISMATCH"A record opens with recorded; amended and closed come after it."Send the first entry as recorded, then the entry you meant, with a new idempotency key.

Any other entry that queued and did not commit carries the sentence "This entry did not commit. Nothing was written, and your earlier entries are unchanged."

Reading

CodeStatusThe rule
RECORD_NOT_FOUND404No record exists for this id in your workspace.
RECORD_ENTRY_NOT_FOUND404No committed entry exists at this position for this record, or in the range asked for.
RECORD_RECEIPT_NOT_READY409No committed entry exists at this position yet; the record has entries still being written.
RECORD_BUNDLE_RANGE_INVALID400fromSeq and toSeq are whole numbers and toSeq is not below fromSeq.
RECORD_RECEIPT_GRANT_REQUIRED, RECORD_BUNDLE_GRANT_REQUIRED403Reading a receipt or a history takes your workspace's key or a grant for this record.
PROOF_MATERIAL_UNAVAILABLE409The sealed record content for this entry is no longer retained. A retention window never removes a record's sealed content, so this is not an answer you should meet; if you do, write to support with the record id.
RECORD_HISTORY_TOO_LARGE413One answer carries a record's history up to 5000 entries on the ledger. The reason reads "This record has more entries than one history can carry." It answers the disclosure-source route and records.disclose; the history route pages instead, naming the next page in X-Next-From-Seq.

Sharing

CodeStatusThe rule
RECORD_ENTRY_NOT_FOUND409This record has no entries yet; a link can be issued once the first entry is queued.
CUSTOMER_LINK_NOT_FOUND404The handle is not a link to this record.
CUSTOMER_LINK_STEP_UP_REQUIRED401On the reader's side: the code that came with the link has not been entered.
RECORDS_SHARE_LINK_EXPIRED410On the reader's side: the link has expired. The sentence names the day and who to ask for a new one.
RECORDS_SHARE_LINK_REVOKED410On the reader's side: the link was turned off. The record it opened has not changed.

What to retry

AnswerRetryHow
A timeout or a dropped connectionYesSame request, same idempotency key.
202 IDEMPOTENCY_PENDINGYesAfter retryAfterSeconds, same request.
429YesAfter retryAfterSeconds, same request.
500, 502, 503, 504YesSame request, same key, with a growing wait.
400, 409, 422 with a codeNoThe sentence names the rule. Fix the entry and send it with a new key.
401, 403, 404NoFix the key, the workspace, or the id.
402NoFix the plan, then send the same request with the same key.

The SDKs retry a timeout, a dropped connection, a 408, a 429, and a 500, 502, 503 or 504 themselves, twice by default, waiting exactly the seconds a 429 names. A 202 they hand back to you, with its retryAfterSeconds, for your code to send again. Bound any loop of your own by attempts and by time.

Reading an error from code

Both SDKs raise a PacSpaceError carrying the status, a code, and the API's sentence. For a refusal with status 400, 403, 409, 410, 413, 422 or 500, the code is the API's code, exactly. For 401, 402, 404, 412, 429 and 503, each SDK raises its own typed error, such as RateLimitError for a 429, and the code it carries is the SDK's own name for that error; the API's code is not passed on. On those, branch on the status and read the API's sentence in the message.

typescript
import { PacSpace, PacSpaceError } from '@pacspace-io/sdk';

const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY! });

try {
  await pac.records.emit({
    record: 'eval-run-4417',
    title: 'Agent ran a shell command that reached outside the test environment',
    kind: 'tool-call',
    occurredAt: '2026-10-05T14:09:47Z',
    actorId: 'agent-7',
    payloads: [ref],                         // from fingerprint(), as in the quick start
    idempotencyKey: 'eval-run-4417:step-2',
  });
} catch (err) {
  if (!(err instanceof PacSpaceError)) throw err;
  console.error(err.statusCode, err.code, err.message);
  // a title over 120 characters: 422 RECORD_TITLE_TOO_LONG A title fits in 120 characters.
}