Guides
Deliver your first message
This walkthrough takes an empty account to a consumer that acknowledges work, in about ten minutes.
Updated 13 August 20268 minute readAPI v2.4
Before you start
You need an API key with the queues:admin and queues:write scopes. Keys are scoped to one environment, and the sandbox key is free: it holds 50,000 messages and drops them after seven days.
export HALYARD_KEY="hk_sandbox_2f9c…"
export API="https://api.halyard.dev/v1"
curl "$API/queues" -H "Authorization: Bearer $HALYARD_KEY"
# {"queues": [], "has_more": false}Every example below is a plain curl. Halyard publishes client libraries for TypeScript, Python and Go, and none of this walkthrough needs one.
Publish a message
A queue is created by name. The visibility timeout you set here is the default for every lease taken against it, and a consumer can shorten it per request.
/queues201curl "$API/queues" \
-H "Authorization: Bearer $HALYARD_KEY" \
-d '{
"name": "orders",
"visibility_timeout": "30s",
"max_deliveries": 5
}'
# 201 Created
# {"name":"orders","region":"eu-west","created_at":"2026-08-13T09:14:22Z"}Publishing stores the body verbatim, up to 256 KB. Halyard answers with 202 once the write is durable in three availability zones, so a successful response means the message survives a node loss.
/queues/orders/messages202curl "$API/queues/orders/messages" \
-H "Authorization: Bearer $HALYARD_KEY" \
-H "Idempotency-Key: 8f14e45f" \
-d '{
"body": {"order_id": "ord_10427", "total_cents": 4990},
"ordering_key": "ord_10427"
}'
# 202 Accepted
# {"id":"msg_01HQ8Z3F","queue":"orders","state":"queued","delivery_count":0}Tip
Lease and acknowledge
A lease is a claim. While it holds, the message is invisible to every other consumer, so two workers cannot pick up the same order. Ask for up to 25 messages at once and set wait to hold the connection open until one arrives.
/queues/orders/leases200curl "$API/queues/orders/leases" \
-H "Authorization: Bearer $HALYARD_KEY" \
-d '{"max": 25, "wait": "20s"}'
# 200 OK
# {
# "lease": "lse_01HQ8Z",
# "expires_at": "2026-08-13T09:15:31Z",
# "messages": [
# {"id":"msg_01HQ8Z3F","body":{"order_id":"ord_10427"},"delivery_count":1}
# ]
# }Acknowledging deletes the message and closes the lease. Acks are idempotent, so a retried ack after a network failure returns 204 rather than an error.
/queues/orders/leases/lse_01HQ8Z/ack204curl "$API/queues/orders/leases/lse_01HQ8Z/ack" \
-X POST -H "Authorization: Bearer $HALYARD_KEY"
# 204 No ContentWork that outlives the timeout should extend the lease rather than race it. One PATCH per half-timeout is the pattern the SDKs use, and it is documented at extend a lease.
How a message moves
Five states, one loop. Everything Halyard guarantees comes from this path, so it is worth reading once before you tune anything.
trigger
POST /messages
The producer sends a body and an idempotency key. Halyard answers 202 once the write is durable.
step
Durable log
The message is written to three availability zones and ordered inside its ordering key.
step
Lease held
A consumer takes the message for visibility_timeout seconds. No one else can see it in that window.
check
Acked before the timeout?
yes
end · success
Deleted
The message leaves the queue and the lease closes. Acks are idempotent.
no
step
Redelivered
delivery_count increments and the backoff doubles, up to five minutes. The message becomes visible again.
returns to Lease held · the next consumer leases it
end · warning
Dead letters
After max_deliveries attempts the message moves to the dead-letter queue with its full history.
Handle failure
A consumer that crashes without acking is not an error condition. The lease expires, the message becomes visible again, and delivery_count increments. Nack when you already know the work failed: it returns the message immediately instead of paying for the whole timeout.
/queues/orders/leases/lse_01HQ8Z/nack204curl "$API/queues/orders/leases/lse_01HQ8Z/nack" \
-X POST -H "Authorization: Bearer $HALYARD_KEY" \
-d '{"retry_after": "60s", "reason": "payment_provider_timeout"}'
# 204 No ContentBackoff doubles from one second and stops at five minutes. After max_deliveries attempts the message moves to the queue’s dead-letter queue with its full delivery history, where you can inspect it and replay it. See dead letters for both endpoints.
Careful
halyard.dead_letters.depth above zero, not on a threshold.Go to production
Four settings separate a working consumer from a production one. The loop below is the shape the TypeScript SDK ships, written out so nothing is hidden.
import { Halyard } from "@halyard/node";
const halyard = new Halyard({ key: process.env.HALYARD_KEY });
for await (const lease of halyard.queue("orders").leases({
max: 25, // batch: one round trip per 25 orders
wait: "20s", // long poll: no busy loop, no idle cost
region: "eu-west" // read from the region that wrote
})) {
for (const message of lease.messages) {
await fulfil(message.body);
}
await lease.ack();
}Ordering keys hold order inside a key and allow parallelism across keys, so one slow customer never blocks another. Retention runs to 14 days on paid plans, and a queue can be paused without losing what is already in it.