Skip to content

SDKs and samples

Updated 2026-08-24

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:

  1. A retry that honours Retry-After with jitter. Every sample below skips this for brevity; production code should not. See Rate limits.
  2. 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-After on 429 and 503, with jitter.
  • Lease extension. If the work can outlast the lease, call extend_lease before it expires rather than discovering the loss at ack time. A lost lease is POSTMQ_LEASE_EXPIRED and the message comes back to someone else — correct behaviour, but not what you wanted.
  • nack on 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/validate runs 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.