Errors and retries

Switch on error codes, not prose.

Every protocol error is shaped for machine handling and support correlation. Human messages may improve over time; stable codes and correlation IDs are the contract.

Body shape{ error: ... }
Headerx-correlation-id
RetriesidempotencyKey

Error shape

All client-visible errors use the same envelope.

{
  "error": {
    "code": "forbidden",
    "message": "GRANT_AGENT_TOKEN Layer 2 consent required",
    "correlationId": "abc123..."
  }
}
Support flow: log the response status, error code, and correlation ID. The correlation ID also appears in the response header and server logs.

Code vocabulary

Current stable codes.

bad_requestMalformed input, invalid JSON, or missing required request fields.
authentication_requiredNo usable identity session or agent token was provided.
payment_requiredSpending gate or payment boundary prevents the requested transition.
forbiddenPermission, consent, data-tier, or actor-policy failure.
not_foundThe resource does not exist, or is intentionally hidden by ownership scope.
conflictState-machine conflict, unique constraint, or incompatible replay.
unprocessable_entityBusiness-rule validation, Sure Price, payer, or policy failure.
rate_limitedRequest or actor exceeded a rate limit; back off before retrying.
internal_errorUnhandled server failure. Only correlation ID is useful to the client.
service_unavailableKnown degraded or unavailable dependency path.

SDK handling

OpenDocError carries status, code, message, and correlation ID.

import { OpenDocError } from "@opendoc/sdk";

try {
  await client.commit(transactionId, "partner-order-123-commit");
} catch (error) {
  if (error instanceof OpenDocError) {
    switch (error.code) {
      case "payment_required":
        // Payment boundary or spending limit needs user action.
        break;
      case "forbidden":
        // Missing consent, permission, or actor scope.
        break;
      case "rate_limited":
        // Back off and retry later.
        break;
      default:
        console.error(error.correlationId);
    }
  }
}

Idempotency

Use stable keys for every state-changing transaction call.

Current transaction routes accept idempotencyKey in the JSON body. The SDK exposes it as an optional argument or input field. Reuse the same key when retrying the same logical step after a timeout or network failure.

await client.declareIntent({
  providerHsoId,
  availabilitySlotId,
  idempotencyKey: "partner-order-123-declare",
});

await client.authorize(transactionId, "partner-order-123-authorize");
await client.acceptTerms(transactionId, "partner-order-123-terms");
await client.commit(transactionId, "partner-order-123-commit");
Do not reuse keys across different logical operations. A declare-intent key, authorize key, terms key, and commit key should be distinct even when they belong to the same partner order.