This is a development version of the documentation. Content may change without notice.
Voke Documentation
ESM Partners

Errors and Retries

Error envelopes, per-surface error codes, retry strategy, idempotency, and observability for ESM partners.

Error envelope

Every Voke REST error response follows the same JSON shape:

{
  "statusCode": 400,
  "message": "Human-readable description",
  "error": "Bad Request",
  "code": "VALIDATION_ERROR",
  "details": ["field: message"]
}

The code field is the stable contract — it is an ApiErrorCode enum string that will not change between minor releases. message and details are informational and may change. Build error-handling logic against code, not message.

For the full list of all codes and their HTTP status mappings, see Reference / Error Codes.

REST errors partners will encounter

These are the ApiErrorCode values a partner is likely to encounter when calling Voke's REST API (API key management, trading config, org-context endpoints).

CodeHTTPWhen it occurs
UNAUTHORIZED401Missing or expired Bearer token / API key
FORBIDDEN403Valid credentials but insufficient permissions for the resource
ORG_CONTEXT_REQUIRED403Request missing the x-org-id header on an org-scoped endpoint
ORG_MEMBERSHIP_REQUIRED403Caller is not a member of the requested organization
INSUFFICIENT_ORG_PERMISSIONS403Caller's role in the org is too low for the action
VALIDATION_ERROR400Request body or query params failed schema validation
INVALID_UUID400A path or query parameter expected a UUID but received something else
NOT_FOUND404Requested resource does not exist (or is not visible to the caller)
CONFLICT409Unique constraint violation (e.g. duplicate API key name)
API_KEY_NOT_FOUND404Revoke/lookup of a non-existent API key
API_KEY_SCOPES_EMPTY400API key creation request contains no scopes
API_KEY_SCOPE_INVALID400One or more requested scopes are not recognized
TRADING_NOT_ENABLED400Trading has not been enabled for the organization
TRADING_CONFIG_INCOMPLETE400Trading is enabled but required config fields (ESM URL, credentials) are missing
ESM_NOT_CONFIGURED400The org has no TradingPartnerConfig row
PLANT_NOT_LINKED_TO_ESM400The plant does not have an external_plant_id set
PLANT_NOT_FOUND404Referenced plant UUID does not exist in the org
TOO_MANY_REQUESTS429Rate limit exceeded; includes Retry-After header in seconds
INTERNAL_SERVER_ERROR500Unexpected server error; safe to retry with backoff

VCP command rejection codes

When Voke rejects a command, the AMQP ack message (event.command-ack) has status: "REJECTED" and a rejectionCode from the RejectionCode enum.

rejectionCodeMeaning
CONSTRAINT_VIOLATIONThe command value violates a site constraint (e.g. setpoint outside allowed range)
INVALID_COMMANDThe command type is not supported for this site/device
INVALID_PAYLOADThe command payload is structurally malformed
DEVICE_OFFLINEThe target device is not reachable
DEVICE_FAULTThe target device is in a fault state and cannot accept commands
UNSUPPORTED_FOR_TOPOLOGYThe command is valid in general but not supported for this plant's physical topology

Never retry a rejected command with the same messageId and the same payload. A REJECTED ack is a semantic refusal, not a transient failure. Change the payload (e.g. bring setpoint within range) or escalate to operations before retrying.

AMQP auth failures

AMQP authentication and authorization failures are not HTTP errors — they occur at the RabbitMQ protocol layer during connection or channel open. Voke uses rabbitmq_auth_backend_http to validate credentials server-side.

FailureVisible asCause
Wrong username (slug) or API keyAMQP 403 Connection refusedCredentials not found or not associated with vcp:connect scope
API key revokedAMQP 403 Connection refusedKey was deleted on the Voke side; re-create and update consumer config
Wrong vhostAMQP 403 Access refusedUse apiKey.vhost when present, otherwise / for legacy keys
Queue name mismatchAMQP 403 Access refusedConsumer declared a queue outside vcp.{slug}.* namespace
Missing publish scopeAMQP 403 Access refusedPublishing route requires a matching vcp:write:* scope
Missing/invalid HMACMessage is accepted by broker then dropped by Vokecommand.device, command.mode, and schedule.* require a valid envelope signature

These failures are logged on the Voke side in the AmqpAuthController. Partners see only the protocol-level refusal. If a connection that previously worked suddenly starts failing, the most likely cause is an API key rotation or revocation — create a new key with vcp:connect scope and update your consumer.

Retry guidance

REST

Response classStrategy
4xx (except 429)Do not retry. Fix the request payload, credentials, or org context.
429 Too Many RequestsWait for the Retry-After value (seconds) from the response header, then retry once.
5xxRetry with exponential backoff: 1 s, 2 s, 4 s, 8 s, 16 s. Max 5 attempts. Abandon and alert if all fail.

VCP (AMQP messages)

ScenarioStrategy
status: "ACCEPTED" or "QUEUED" ack receivedWait for event.execution before concluding the command outcome.
status: "PARTIAL" ack receivedInspect results[]; some batch items were accepted and some rejected.
status: "REJECTED" ack receivedDo not retry. Change payload or escalate.
No ack within your timeoutCheck whether the messageId was already processed (consult your own log). If not, you may retry with a new messageId after confirming the site is reachable.
Consumer reconnectRabbitMQ will redeliver all unacknowledged messages. Expect duplicates. Dedupe by messageId on the consumer side.
event.execution delayed or absentThis is a fan-out timing issue on Voke's side — do not speculatively re-send the command. Wait at least 30 s for late event.execution delivery before escalating.

AMQP connection drops

Reconnect immediately (no wait on first attempt), then apply backoff on subsequent failures. Queues are durable and messages are persistent — nothing is lost while you are disconnected. Unacked messages redeliver automatically when you reconnect.

Idempotency

  • Always set a unique messageId in every VCP envelope you send. Use a UUID v4 or a collision-resistant identifier. The messageId is your primary handle for correlating acks and statuses.
  • Voke deduplicates inbound commands by messageId for 10 minutes via Redis (vcp:seen:{orgSlug}:{messageId}, atomic SET NX EX 600). The first delivery proceeds; duplicates inside the window are acked at the broker and silently dropped — no ACK is published back to the partner for a replay, so do not wait on one. If downstream processing throws, Voke releases the claim so a legitimate retry with the same messageId proceeds. Outside the 10-minute window the same messageId will be reprocessed — design commands to be semantically idempotent (scheduleId, correlationId, stable command refs) rather than relying on the replay guard alone. See VCP message integrity for the full replay-guard contract.
  • Partners must deduplicate inbound events by messageId locally. After a consumer reconnect, RabbitMQ may redeliver the most recently unacknowledged batch. A simple in-memory LRU set of the last N messageId values is sufficient for most cases.

Observability

Partners typically log every inbound ack and status event keyed by correlationId so the complete command round-trip is reconstructable from partner logs alone:

[correlationId: req-abc123] SEND  command.site-setpoint  messageId: msg-001
[correlationId: req-abc123] ACK   QUEUED                 messageId: msg-001-ack
[correlationId: req-abc123] STATUS EXECUTING             messageId: msg-001-status-1
[correlationId: req-abc123] STATUS COMPLETED             messageId: msg-001-status-2

On Voke's side, VcpCommandLog persists the same timeline — every command, ack, and status event is a row keyed by messageId and correlationId. When filing an incident, provide the messageId and Voke ops can find the corresponding VcpCommandLog rows within seconds.

On this page