Menu

Queues

Durable queue rung for agent-native ingest, webhook fan-out, and anything that needs an addressable inbox by URL. Per-queue Bearer auth (queue_api_key, returned ONCE at creation). Server-assigned monotonic seq; IETF Idempotency-Key dedupe on publish; per-row DLQ replay; reserved _dash_ consumer prefix for dashboard inspection. See /llms-full.txt for the cold-LLM long-form spec.

Quickstart

# 1. Create a queue (no auth — anonymous)
curl -X POST https://api.gopigeon.dev/q/create \
  -H 'Content-Type: application/json' \
  -d '{"consumers":["me"]}'
# → capture queue_id + queue_api_key from response
QUEUE_ID=q_...
QUEUE_API_KEY=qk_...

# 2. Publish a message
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/publish \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"}'
# → 201 {"seq":1,...}

# 3. Pull messages (consumer_id required; cursor=0 reads from head)
curl -H "Authorization: Bearer $QUEUE_API_KEY" \
  "https://api.gopigeon.dev/q/$QUEUE_ID/pull?consumer_id=me&cursor=0"
# → 200 {"messages":[...],"next_cursor":1,...}

# 4. Ack the cursor to advance position
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/ack \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"cursor":1,"consumer_id":"me"}'
# → 200 {"ok":true,"cursor":1,...}

Free vs Pro

Every gopigeon user gets a durable queue rung. The free tier is rate-limited; Pro unlocks the higher cap.

Free tier: 100 messages/month per user across all queues. Once a user crosses 100 publishes in a calendar month, every additional POST {PATH}/publish returns:

{
  "error": "monthly queue publish quota exceeded",
  "plan": "free",
  "queue_id": "q_...",
  "current": 100,
  "limit": 100,
  "upgrade_url": "https://gopigeon.dev/dashboard/billing",
  "auto_enqueued": true,
  "will_replay_on_upgrade": true,
  "pending_publish_id": "pp_<nanoid>",
  "_help": "Monthly queue publish quota exceeded. Your message was queued and will replay automatically when you upgrade to Pro.",
  "_links": { /* … */ }
}

The HTTP status is 402 Payment Required. The envelope is NEVER silent-droppedauto_enqueued: true means the publish was persisted server-side to a queue_pending_publishes table. When the user upgrades to Pro via the Stripe checkout flow, the backend’s webhook fires an async drain job within 60 seconds that replays every stranded envelope to AMQP in original FIFO order. The consumer sees the messages in seq order after upgrade, as if the 402 window never happened.

For owned queues (any queue where the first authenticated Bearer /q/{id}/* call set the queue’s owner), the counter is per-user across every queue they own. will_replay_on_upgrade is true and the drain runs on upgrade.

For anonymous queues (never claimed — no authenticated Bearer call has ever hit any /q/{id}/* route on it), a separate per-source-IP counter applies. The envelope still persists with auto_enqueued: true, but will_replay_on_upgrade is false: the stranded rows stay dormant until and unless someone claims the queue (by hitting any /q/{id}/* endpoint with both a queue_api_key AND a session cookie) AND that owner upgrades.

TierQuotaBehavior on exceed
Free100 messages/month per user across all owned queues402 with auto_enqueued: true, drains on upgrade
Pro10,000 messages/month per user across all owned queuesSame 402 shape if Pro user crosses 10k; rare

See /pricing for the full plan comparison. The 402 response carries an upgrade_url field that points directly to the dashboard billing flow.

POST /q/create

Anonymous; creates a new durable queue and returns the plaintext queue_api_key ONCE. The key is the Bearer credential for every subsequent {PATH}/* call — store it immediately.

# Create a new durable queue (no auth required)
curl -X POST https://api.gopigeon.dev/q/create \
  -H 'Content-Type: application/json' \
  -d '{"consumers":["me"]}'
# → 201 {"queue_id":"q_abc123def456abcd","queue_api_key":"qk_…64hex","consumers":["me"],"publish_url":"https://api.gopigeon.dev/q/q_.../publish","pull_url":"https://api.gopigeon.dev/q/q_.../pull?cursor=0&consumer_id=me","_help":"Queue created. Capture queue_api_key NOW..."}

Help: Queue created. Capture queue_api_key NOW — it is shown only once. Use it as Authorization: Bearer <key> on every /q/{id}/* call.

POST /q/{id}/publish

Append a JSON message. Server assigns a monotonic seq per queue. Pass an Idempotency-Key header for IETF draft-07 dedupe within a 24h window.

# Publish a JSON message to the queue (requires queue_api_key)
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/publish \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"}'
# → 201 {"seq":42,"id":"qm_abc123def456abcd","_help":"Message accepted...","_links":{...}}

Help: Message accepted. Pass the returned seq to GET /q/{id}/pull?cursor=<seq-1>&consumer_id=<x> to read it back; ack with POST /q/{id}/ack {cursor:<seq>, consumer_id:<x>}.

Cursors are exclusive: seq > cursor. Initial cursor is 0. next_cursor in each response = highest seq returned. Pass it as the next request's cursor.

GET /q/{id}/pull

Read up to limit messages with seq > cursor for a given consumer_id. Cursor is NOT advanced by pull — call /ack after processing. Each response carries a next_url you can re-fetch verbatim to paginate.

# Pull up to N messages with seq > cursor
curl -H "Authorization: Bearer $QUEUE_API_KEY" \
  "https://api.gopigeon.dev/q/$QUEUE_ID/pull?consumer_id=$CONSUMER_ID&cursor=0&limit=10"
# → 200 {"messages":[...envelope],"next_cursor":42,"has_more":false,"next_url":"...","_help":"Returns up to 'limit' messages..."}

Help: Returns up to 'limit' messages with seq > cursor. Save next_cursor and POST /q/{id}/ack {cursor:next_cursor, consumer_id:X} when done. has_more=true means more available; pull again with cursor=next_cursor.

POST /q/{id}/ack

Advance the stored cursor for a consumer_id to (at most) the supplied value. UPSERT MAX semantics — re-acking with a lower cursor is a no-op. Per-consumer cursors gate GC: messages with seq <= min(cursor) across registered consumers may be reaped.

# Acknowledge messages up to and including a cursor (UPSERT MAX)
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/ack \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"cursor":42,"consumer_id":"me"}'
# → 200 {"ok":true,"cursor":42,"_help":"Cursor advanced (UPSERT MAX...)"}

Help: Cursor advanced (UPSERT MAX — sending a lower cursor is a no-op). Future pulls will skip messages with seq <= cursor.

GET /q/{id}/consumers

List the registered consumer roster. Returns the canonical set used for GC gating and _dash_ filtering on dashboard preview.

# List registered consumers for the queue
curl -H "Authorization: Bearer $QUEUE_API_KEY" \
  https://api.gopigeon.dev/q/$QUEUE_ID/consumers
# → 200 {"queue_id":"q_...","consumers":["me","pidge"],"_help":"Consumer roster..."}

Help: Consumer roster updated. New consumers see ONLY future messages (RabbitMQ semantics — no historical replay). Removed consumers stop gating GC immediately.

POST /q/{id}/consumers

Mutate the consumer roster (add and/or remove). New consumers see ONLY future messages (RabbitMQ semantics — no historical replay). Removed consumers stop gating GC immediately.

# Add or remove consumers (mutates the registered set)
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/consumers \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"add":["new_consumer"],"remove":["old_consumer"]}'
# → 200 {"queue_id":"q_...","consumers":["me","new_consumer"],"_help":"Consumer roster updated..."}

Help: Consumer roster updated. New consumers see ONLY future messages (RabbitMQ semantics — no historical replay). Removed consumers stop gating GC immediately.

POST /q/{id}/origins

Mutate the allowed-origins list. CORS preflight will accept the new set immediately for OPTIONS {PATH}/* — no restart required. Additive only; the * wildcard cannot be set from this endpoint (re-create the queue with allowed_origins=*).

# Mutate the allowed-origins list (additive only — wildcard cannot be set here)
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/origins \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"add":["https://example.com"],"remove":[]}'
# → 200 {"queue_id":"q_...","allowed_origins":"https://example.com","_help":"Allowed-origins list updated..."}

Help: Allowed-origins list updated. CORS preflight will accept the new set immediately for OPTIONS /q/{id}/* — no restart required. The * wildcard cannot be set from this endpoint; re-create the queue with allowed_origins=* to unrestrict.

GET /q/{id}/dlq

List Dead-Letter-Queue messages. Pagination is approximate — DLQ listing requeues tags so subsequent requests may see the same set.

# List Dead-Letter-Queue messages (pagination is approximate)
curl -H "Authorization: Bearer $QUEUE_API_KEY" \
  https://api.gopigeon.dev/q/$QUEUE_ID/dlq
# → 200 {"messages":[...envelope with nack_count],"_help":"Replay a message via POST..."}

Help: Replay a message via POST /q/{queue_id}/dlq/replay {dlq_msg_id: <envelope.id>}. Replay creates a NEW envelope with source=dlq-replay (and replayed_from preserving the original id); the DLQ message is acked once replayed. Pagination is approximate — DLQ listing requeues tags so subsequent requests may see the same set.

Replay is per-row, NOT idempotent. The same message replayed twice produces TWO new envelopes. Track in your application by the replayed_from field on envelopes.

POST /q/{id}/dlq/replay

Replay a single DLQ message back into the main queue. Creates a NEW envelope with source=dlq-replay and replayed_from preserving the original id; the DLQ message is acked once replayed.

# Replay a single DLQ message back into the main queue (per-row, not idempotent)
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/dlq/replay \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"dlq_msg_id":"qm_abc123"}'
# → 200 {"ok":true,"replayed":{...new envelope, source:'dlq-replay', replayed_from:'qm_abc123'},"_help":"Replayed envelope published..."}

Help: Replayed envelope published to the main queue. Every registered consumer can pull it (cursor < new seq). Source=dlq-replay; replayed_from preserves the original id for audit.

OpenAPI spec

For typed-client generation or schema-aware tooling, see https://api.gopigeon.dev/openapi.json — every route documented here is in the spec. The OpenAPI server is the source-of-truth; this page mirrors its /q/* surface for cold-LLM consumption.

Worked example: form → queue → consumer pull

Bridges the v1.0 forms model into the v1.1 queue model. Start with a form you already own (form_id + form_api_key from POST /new), bind it to a fresh queue via the subscribe_form_to_queue MCP tool, and consume submissions as queue envelopes.

# Step 1: You already have a v1.0 form (form_id + form_api_key from /new)
FORM_ID=f_yourformhere
FORM_API_KEY=fk_yoursecrethere

# Step 2: Create a queue (no auth)
curl -X POST https://api.gopigeon.dev/q/create \
  -H 'Content-Type: application/json' \
  -d '{"consumers":["me"]}'
# → capture queue_id and queue_api_key from response
QUEUE_ID=q_capturedfromabove
QUEUE_API_KEY=qk_capturedfromabove

# Step 3: Bind the form to the queue via the subscribe_form_to_queue MCP tool.
# Requires GOPIGEON_API_KEY (forms-side auth — NOT GOPIGEON_QUEUE_API_KEY).
# In your MCP client (Claude Code, Cursor, etc.) the tool call looks like:
#
#   {"name":"subscribe_form_to_queue","arguments":{"form_id":"$FORM_ID","queue_id":"$QUEUE_ID"}}
#
# REST alternative (dashboard session-auth; not in /openapi.json):
#   PUT https://api.gopigeon.dev/api/forms/$FORM_ID  body {"queue_id":"$QUEUE_ID"}

# Step 4: Submit to the form. Non-spam submissions now mirror into the queue.
curl -X POST https://api.gopigeon.dev/f/$FORM_ID \
  -d 'name=Jane&[email protected]&message=hello'
# → 200 {"ok":true,"submission_id":"sub_..."}

# Step 5: Pull the mirrored submission from the queue
curl -H "Authorization: Bearer $QUEUE_API_KEY" \
  "https://api.gopigeon.dev/q/$QUEUE_ID/pull?consumer_id=me&cursor=0"
# → 200 {"messages":[{...envelope, source:'form-mirror', form_id:'$FORM_ID', payload:{...submission}}],"next_cursor":1,...}

# Step 6: Ack the cursor to advance the consumer's position
curl -X POST https://api.gopigeon.dev/q/$QUEUE_ID/ack \
  -H "Authorization: Bearer $QUEUE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"cursor":1,"consumer_id":"me"}'
# → 200 {"ok":true,"cursor":1,...}

Env-var asymmetry: subscribe_form_to_queue requires GOPIGEON_API_KEY (forms-side, account-scoped) — NOT GOPIGEON_QUEUE_API_KEY (per-queue Bearer). Every other queue MCP tool uses the queue key. Setting the wrong env var produces a 401 with no obvious recovery hint.

Not what you're looking for? See the full surface at /llms.txt.