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).
| Code | HTTP | When it occurs |
|---|---|---|
UNAUTHORIZED | 401 | Missing or expired Bearer token / API key |
FORBIDDEN | 403 | Valid credentials but insufficient permissions for the resource |
ORG_CONTEXT_REQUIRED | 403 | Request missing the x-org-id header on an org-scoped endpoint |
ORG_MEMBERSHIP_REQUIRED | 403 | Caller is not a member of the requested organization |
INSUFFICIENT_ORG_PERMISSIONS | 403 | Caller's role in the org is too low for the action |
VALIDATION_ERROR | 400 | Request body or query params failed schema validation |
INVALID_UUID | 400 | A path or query parameter expected a UUID but received something else |
NOT_FOUND | 404 | Requested resource does not exist (or is not visible to the caller) |
CONFLICT | 409 | Unique constraint violation (e.g. duplicate API key name) |
API_KEY_NOT_FOUND | 404 | Revoke/lookup of a non-existent API key |
API_KEY_SCOPES_EMPTY | 400 | API key creation request contains no scopes |
API_KEY_SCOPE_INVALID | 400 | One or more requested scopes are not recognized |
TRADING_NOT_ENABLED | 400 | Trading has not been enabled for the organization |
TRADING_CONFIG_INCOMPLETE | 400 | Trading is enabled but required config fields (ESM URL, credentials) are missing |
ESM_NOT_CONFIGURED | 400 | The org has no TradingPartnerConfig row |
PLANT_NOT_LINKED_TO_ESM | 400 | The plant does not have an external_plant_id set |
PLANT_NOT_FOUND | 404 | Referenced plant UUID does not exist in the org |
TOO_MANY_REQUESTS | 429 | Rate limit exceeded; includes Retry-After header in seconds |
INTERNAL_SERVER_ERROR | 500 | Unexpected 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.
rejectionCode | Meaning |
|---|---|
CONSTRAINT_VIOLATION | The command value violates a site constraint (e.g. setpoint outside allowed range) |
INVALID_COMMAND | The command type is not supported for this site/device |
INVALID_PAYLOAD | The command payload is structurally malformed |
DEVICE_OFFLINE | The target device is not reachable |
DEVICE_FAULT | The target device is in a fault state and cannot accept commands |
UNSUPPORTED_FOR_TOPOLOGY | The 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.
| Failure | Visible as | Cause |
|---|---|---|
| Wrong username (slug) or API key | AMQP 403 Connection refused | Credentials not found or not associated with vcp:connect scope |
| API key revoked | AMQP 403 Connection refused | Key was deleted on the Voke side; re-create and update consumer config |
| Wrong vhost | AMQP 403 Access refused | Use apiKey.vhost when present, otherwise / for legacy keys |
| Queue name mismatch | AMQP 403 Access refused | Consumer declared a queue outside vcp.{slug}.* namespace |
| Missing publish scope | AMQP 403 Access refused | Publishing route requires a matching vcp:write:* scope |
| Missing/invalid HMAC | Message is accepted by broker then dropped by Voke | command.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 class | Strategy |
|---|---|
| 4xx (except 429) | Do not retry. Fix the request payload, credentials, or org context. |
| 429 Too Many Requests | Wait for the Retry-After value (seconds) from the response header, then retry once. |
| 5xx | Retry 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)
| Scenario | Strategy |
|---|---|
status: "ACCEPTED" or "QUEUED" ack received | Wait for event.execution before concluding the command outcome. |
status: "PARTIAL" ack received | Inspect results[]; some batch items were accepted and some rejected. |
status: "REJECTED" ack received | Do not retry. Change payload or escalate. |
| No ack within your timeout | Check 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 reconnect | RabbitMQ will redeliver all unacknowledged messages. Expect duplicates. Dedupe by messageId on the consumer side. |
event.execution delayed or absent | This 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
messageIdin every VCP envelope you send. Use a UUID v4 or a collision-resistant identifier. ThemessageIdis your primary handle for correlating acks and statuses. - Voke deduplicates inbound commands by
messageIdfor 10 minutes via Redis (vcp:seen:{orgSlug}:{messageId}, atomicSET 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 samemessageIdproceeds. Outside the 10-minute window the samemessageIdwill 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
messageIdlocally. After a consumer reconnect, RabbitMQ may redeliver the most recently unacknowledged batch. A simple in-memory LRU set of the last NmessageIdvalues 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-2On 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.
Cross-links
- Reference / Error Codes — full
ApiErrorCodetable with HTTP status and descriptions - Commands — command types, routing keys, ack flow
- VCP message integrity — envelope structure and signing