SDKs and samples
PostMQ ships no client library, because it does not need one. Every route is JSON over HTTP with a bearer token, and the whole send–receive–acknowledge loop is about forty lines in any language.
There is no SDK, on purpose
A client library would be a thin wrapper over POST with a bearer header, and a second thing to version. The
samples below are the whole surface: send, claim under a lease, acknowledge.
Three facts shape every sample below. A send commits atomically — envelope, idempotency record, audit row and outbox row in one transaction, or none of them. Idempotency keys are retained for 24 hours and fingerprinted over the request body, so replaying one with the same body returns the original response rather than sending twice. And delivery is at-least-once, so a handler must be safe to run on the same message more than once.
Two things are worth wrapping in your own code, whatever language you use:
- A retry that honours
Retry-Afterwith jitter. Every sample below skips this for brevity; production code should not. See Rate limits. - An idempotency key per logical send. Generate it where the intent is formed, not at the call site — that way a retry of the whole operation reuses the key instead of minting a new one and sending twice.
If your caller is an agent rather than a program, use MCP instead and skip all of this — the client does the transport for you. See the MCP guide.
The pmq CLI
For shell work there is a small CLI: pmq send, pmq pending, pmq ack, pmq nack, and pmq mcp stdio for the
stdio transport. Build it from source — packaged downloads are not yet published.
bash
#!/usr/bin/env bash
set -euo pipefail
PMQ="https://api.postmq.com/v1"
TOKEN="${POSTMQ_CREDENTIAL:?set POSTMQ_CREDENTIAL}"
auth=(-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json')
# 1. Send. The idempotency key is mandatory.
curl -sS -X POST "$PMQ/messages/send" "${auth[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"envelope": {
"recipient": { "friendly_name": "reviewer" },
"template": "directive"
},
"payload": {
"directive_kind": "review",
"summary": "Review the lease-expiry sweep",
"instructions": "Report anything that could re-pend a live lease."
}
}' | jq .
# 2. Claim. Long-polls up to 20 s so an idle consumer is not spinning.
# The response is { items: [ { envelope, payload, lease } ], … }.
batch=$(curl -sS -X POST "$PMQ/messages/get-pending" "${auth[@]}" \
-d '{ "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }')
# 3. Acknowledge each one with the lease it came with.
echo "$batch" | jq -c '.items[]?' | while read -r item; do
id=$(echo "$item" | jq -r '.envelope.message_id')
lease=$(echo "$item" | jq -r '.lease.lease_id')
# ... do the work ...
curl -sS -X POST "$PMQ/messages/$id/ack" "${auth[@]}" \
-d "$(jq -nc --arg l "$lease" '{lease_id: $l, outcome_summary: "Done."}')"
done
Python
Standard library only — no dependencies.
import json, os, urllib.request, uuid
BASE = "https://api.postmq.com/v1"
TOKEN = os.environ["POSTMQ_CREDENTIAL"]
def call(path, body, idempotency_key=None):
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
req = urllib.request.Request(
f"{BASE}{path}", data=json.dumps(body).encode(), headers=headers, method="POST"
)
with urllib.request.urlopen(req) as resp:
return json.load(resp)
# 1. Send. Mint the key where the intent is formed, so a retry of the whole
# operation reuses it rather than sending a second message.
key = str(uuid.uuid4())
call(
"/messages/send",
{
"envelope": {
"recipient": {"friendly_name": "reviewer"},
"template": "directive",
},
"payload": {
"directive_kind": "review",
"summary": "Review the lease-expiry sweep",
"instructions": "Report anything that could re-pend a live lease.",
},
},
idempotency_key=key,
)
# 2. Claim under a lease, long-polling for up to 20 seconds. Each item is
# {"envelope": {...}, "payload": {...}, "lease": {"lease_id": ..., "lease_expires_at": ...}}.
batch = call(
"/messages/get-pending",
{"max": 10, "visibility_timeout": "PT2M", "wait": "PT20S"},
)
# 3. Work each message, then acknowledge with the lease it arrived with.
# Delivery is at-least-once, so handle() must be safe to run twice.
for item in batch.get("items", []):
handle(item["envelope"], item["payload"])
call(
f"/messages/{item['envelope']['message_id']}/ack",
{"lease_id": item["lease"]["lease_id"], "outcome_summary": "Done."},
)
TypeScript
const BASE = 'https://api.postmq.com/v1';
const TOKEN = process.env.POSTMQ_CREDENTIAL!;
async function call<T>(path: string, body: unknown, idempotencyKey?: string): Promise<T> {
const headers: Record<string, string> = {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
};
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const res = await fetch(`${BASE}${path}`, { method: 'POST', headers, body: JSON.stringify(body) });
if (!res.ok) {
// Branch on error.code — never on the message, which is prose and may be reworded.
const { error, request_id } = await res.json();
throw new Error(`${error.code} (${request_id}): ${error.message}`);
}
return res.json() as Promise<T>;
}
// 1. Send.
await call('/messages/send', {
envelope: { recipient: { friendly_name: 'reviewer' }, template: 'directive' },
payload: {
directive_kind: 'review',
summary: 'Review the lease-expiry sweep',
instructions: 'Report anything that could re-pend a live lease.',
},
}, crypto.randomUUID());
// 2. Claim under a lease.
interface PendingItem {
envelope: { message_id: string; template: string };
payload: unknown;
lease: { lease_id: string; lease_expires_at: string };
}
const batch = await call<{ items: PendingItem[] }>(
'/messages/get-pending',
{ max: 10, visibility_timeout: 'PT2M', wait: 'PT20S' },
);
// 3. Acknowledge. At-least-once delivery — make handle() safe to run twice.
for (const item of batch.items ?? []) {
await handle(item.envelope, item.payload);
await call(`/messages/${item.envelope.message_id}/ack`, {
lease_id: item.lease.lease_id,
outcome_summary: 'Done.',
});
}
C#
using System.Net.Http.Headers;
using System.Net.Http.Json;
var token = Environment.GetEnvironmentVariable("POSTMQ_CREDENTIAL")!;
using var http = new HttpClient { BaseAddress = new Uri("https://api.postmq.com/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// 1. Send. The idempotency key is mandatory; mint it with the intent.
var send = new HttpRequestMessage(HttpMethod.Post, "messages/send")
{
Content = JsonContent.Create(new
{
envelope = new
{
recipient = new { friendly_name = "reviewer" },
template = "directive",
},
payload = new
{
directive_kind = "review",
summary = "Review the lease-expiry sweep",
instructions = "Report anything that could re-pend a live lease.",
},
}),
};
send.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
(await http.SendAsync(send)).EnsureSuccessStatusCode();
// 2. Claim under a lease, long-polling for up to 20 seconds.
// PendingBatch mirrors { items: [ { envelope, payload, lease } ] }.
var batch = await (await http.PostAsJsonAsync(
"messages/get-pending",
new { max = 10, visibility_timeout = "PT2M", wait = "PT20S" }))
.Content.ReadFromJsonAsync<PendingBatch>();
// 3. Acknowledge each with the lease it arrived with.
foreach (var item in batch?.Items ?? [])
{
await Handle(item.Envelope, item.Payload);
await http.PostAsJsonAsync(
$"messages/{item.Envelope.MessageId}/ack",
new { lease_id = item.Lease.LeaseId, outcome_summary = "Done." });
}
What the samples leave out
Deliberately, so the shape stays readable. Add these before you run anything in production:
- Retry and backoff. Honour
Retry-Afteron429and503, with jitter. - Lease extension. If the work can outlast the lease, call
extend_leasebefore it expires rather than discovering the loss atacktime. A lost lease isPOSTMQ_LEASE_EXPIREDand the message comes back to someone else — correct behaviour, but not what you wanted. nackon failure. A message you cannot handle should be declined with a redelivery time, not dropped on the floor to wait out its lease.- Validation. If the payload is generated,
POST /v1/messages/validateruns the same checks without enqueuing anything or spending an idempotency key. - Error handling by
code. Errors lists which codes are retryable and which are terminal.