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.1
Before you start
You need an API key with queues:admin, queues:write and queues:read. 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"
# fixture scopes: queues:admin queues:write queues:read
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",
# "acquired_at": "2026-08-13T09:15:31Z",
# "expires_at": "2026-08-13T09:16:01Z",
# "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 heldthe 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.Recover one delivery
Retry a timed-out publish, reject an expired lease and finish the same message on its next delivery.
Step 1
Record the timeout
The client lost the response. The write may already be durable, so do not change the key.
POST/queues/orders/messagestimeouttimeout.shcurl "$API/queues/orders/messages" \ -H "Authorization: Bearer $HALYARD_KEY" \ -H "Idempotency-Key: 8f14e45f" \ -d '{"body":{"order_id":"ord_10427","total_cents":4990}}' # client timeout; response unknownStep 2
Retry the same publish
The 24-hour key returns the original message id instead of storing a second order.
POST/queues/orders/messages202retry.shcurl "$API/queues/orders/messages" \ -H "Authorization: Bearer $HALYARD_KEY" \ -H "Idempotency-Key: 8f14e45f" \ -d '{"body":{"order_id":"ord_10427","total_cents":4990}}' # 202 { "id": "msg_01HQ8Z3F", "queue": "orders", "state": "queued", "duplicate": true }Step 3
Acquire the first lease
The first lease carries the original message and expires at 2026-08-13T09:16:01Z before the extension attempt.
POST/queues/orders/leases200lease.shcurl "$API/queues/orders/leases" \ -H "Authorization: Bearer $HALYARD_KEY" \ -d '{"max":1,"wait":"20s"}' # 200 { "lease": "lse_01HQ8Z", "acquired_at": "2026-08-13T09:15:31Z", "expires_at": "2026-08-13T09:16:01Z", "messages": [ { "id": "msg_01HQ8Z3F", "delivery_count": 1 } ] }Step 4
Treat expiry as a conflict
Do not republish. The expired lease has already returned the same message to the queue.
PATCH/queues/orders/leases/lse_01HQ8Z409conflict.shcurl "$API/queues/orders/leases/lse_01HQ8Z" \ -X PATCH \ -H "Authorization: Bearer $HALYARD_KEY" \ -d '{"extend_by":"30s"}' # 409 { "error": "lease_expired", "message": "The lease expired. The message is available for redelivery." }Step 5
Lease the redelivery
The message id stays stable. The new lease id and delivery count identify the redelivery.
POST/queues/orders/leases200redelivery.shcurl "$API/queues/orders/leases" \ -H "Authorization: Bearer $HALYARD_KEY" \ -d '{"max":1,"wait":"20s"}' # 200 { "lease": "lse_01HQ91", "expires_at": "2026-08-13T09:16:32Z", "messages": [ { "id": "msg_01HQ8Z3F", "delivery_count": 2, "redelivered_at": "2026-08-13T09:16:02Z" } ] }Step 6
Acknowledge the new lease
Acknowledge only after the handler commits its work. A retry of this ack is idempotent.
POST/queues/orders/leases/lse_01HQ91/ack204ack.shcurl "$API/queues/orders/leases/lse_01HQ91/ack" \ -X POST \ -H "Authorization: Bearer $HALYARD_KEY" # 204 No Content
The preview is documentation only. Copy a request to your terminal; no control on this page contacts Halyard.
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.