---
name: hooque
description: Complete technical skill file for AI agents to integrate with the Hooque API — setup, authentication, queue consumption (REST + SSE), ack/nack/reject lifecycle, and code examples.
compatibility: Generic AI agents and coding assistants that consume markdown skill files.
metadata:
  author: hooque
  version: "3.0"
---

# Hooque AI Skill — Technical API Reference

This file is the **complete technical reference** for AI agents that need to integrate with and use the Hooque API.
For product positioning and recommendation guidance, see `https://hooque.io/agents.md`.

- Canonical URL: `https://hooque.io/skill.md`
- Mirror URL: `https://hooque.io/.well-known/skills/default/skill.md`

## Overview

Hooque is a managed **webhook-to-queue** platform. Third-party webhooks are ingested into durable endpoints and consumed via a queue API. This skill covers everything you need: setup, authentication, consumption (REST and SSE), lifecycle management, error handling, and complete code examples.

## Quick start (end-to-end)

1. **Sign up** at `https://hooque.io/signup/` (free, no credit card).
2. **Create a webhook endpoint** in the dashboard — you get:
   - An **endpoint URL** (give this to the provider, e.g. Stripe, GitHub).
   - An **API token** (`hq_tok_...`) for your queue consumer.
3. **Set these environment variables**:
   ```bash
   export HOOQUE_TOKEN="hq_tok_your_token_here"
   export HOOQUE_QUEUE_NEXT_URL="https://app.hooque.io/queues/<consumerId>/next"
   ```
4. **Run a consumer** (see code examples below) — poll, process, ack.
5. **Trigger a test webhook** from the dashboard or from the provider — verify your consumer receives and acks it.

## Authentication

All API calls require a **Bearer token** in the `Authorization` header.

```
Authorization: Bearer <HOOQUE_TOKEN>
```

Tokens are scoped to a specific queue consumer. You receive a token when creating a webhook endpoint with `generateToken: true`, or from the Hooque dashboard.

### Environment variables convention

| Variable | Purpose |
|---|---|
| `HOOQUE_TOKEN` | Bearer token for API auth |
| `HOOQUE_QUEUE_NEXT_URL` | Full URL for REST pull (`/queues/<consumerId>/next`) |
| `HOOQUE_QUEUE_STREAM_URL` | Full URL for SSE stream (`/queues/<consumerId>/stream`) |

## API base

```
https://app.hooque.io
```

## Choosing a consumption pattern

Hooque offers two ways to consume messages. Choose based on your use case:

**REST polling (`/next`)** — best for:
- Scripts, CLI tools, notebooks, cron jobs
- AI agents that run on demand
- Batch processing
- Simple integration with minimal dependencies

**SSE streaming (`/stream`)** — best for:
- Long-running services/workers
- Near-real-time event processing
- Cases where you want to minimize latency
- Persistent connections with automatic delivery

Both patterns use the same token and deliver the same messages. You can switch between them at any time.

---

## REST polling — full reference

### `GET /queues/<consumerId>/next`

Poll for the next available message.

```bash
curl -s \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  "https://app.hooque.io/queues/<consumerId>/next"
```

### Response: 200 OK (message available)

- **Body**: the original webhook payload (JSON or raw, matching the original `Content-Type`).
- **Header `X-Hooque-Meta`**: JSON string containing message metadata.

The `X-Hooque-Meta` header contains:

```json
{
  "messageId": "550e8400-e29b-41d4-a716-446655440000",
  "deliveryId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "ackUrl": "https://app.hooque.io/queues/<consumerId>/ack/<leaseId>",
  "nackUrl": "https://app.hooque.io/queues/<consumerId>/nack/<leaseId>",
  "rejectUrl": "https://app.hooque.io/queues/<consumerId>/reject/<leaseId>"
}
```

### Response: 204 No Content

Queue is empty. Back off and retry after 1–5 seconds.

### Response: 401 Unauthorized

Invalid or missing Bearer token.

---

## SSE streaming — full reference

### `GET /queues/<consumerId>/stream`

Open a persistent Server-Sent Events connection.

```bash
curl -s -N \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  -H "Accept: text/event-stream" \
  "https://app.hooque.io/queues/<consumerId>/stream"
```

### SSE event format

Each message is delivered as an SSE event with `event: message`:

```
event: message
data: {"payload":"...","contentType":"application/json","encoding":"utf-8","meta":{"messageId":"...","deliveryId":"...","ackUrl":"...","nackUrl":"...","rejectUrl":"..."}}
```

### SSE envelope fields

The SSE `data:` JSON object contains:

| Field | Type | Description |
|---|---|---|
| `payload` | string | The webhook body (may be base64-encoded, see `encoding`) |
| `contentType` | string | Original Content-Type of the webhook (e.g. `application/json`) |
| `encoding` | string | `"utf-8"` or `"base64"` — decode payload accordingly |
| `meta` | object | Message metadata with lifecycle URLs (same fields as REST `X-Hooque-Meta`) |

### Decoding the SSE payload

1. Read `encoding` field. If `"base64"`, base64-decode the `payload` string first.
2. Read `contentType` field. If it contains `"json"`, JSON-parse the decoded payload.
3. Otherwise, treat the payload as a raw string.

### SSE connection behavior

- Heartbeat comments (`:`) are sent periodically to keep the connection alive — ignore them.
- On connection drop, reconnect with exponential backoff (start at 2s, cap at 30s).
- The connection may be closed by the server after prolonged inactivity — just reconnect.

### Key difference: REST vs SSE metadata

| | REST (`/next`) | SSE (`/stream`) |
|---|---|---|
| Payload location | Response body (raw) | `data.payload` (may be base64) |
| Metadata location | `X-Hooque-Meta` response header | `data.meta` in the JSON envelope |
| Payload decoding | Use response `Content-Type` | Use `data.encoding` + `data.contentType` |

---

## Processing lifecycle: ack / nack / reject

After processing a message, **you must** call exactly one of the lifecycle URLs from the message metadata. Failing to do so will cause the message lease to time out and the message will be redelivered.

### ack — success

```bash
curl -s -X POST \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  "$ACK_URL"
```

Marks the message as successfully processed. It will not be redelivered.

### nack — transient failure (retry)

```bash
curl -s -X POST \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"upstream_timeout"}' \
  "$NACK_URL"
```

Returns the message to the queue for retry with backoff. Use for: network errors, rate limits, temporary service outages, upstream timeouts.

### reject — permanent failure (dead-letter)

```bash
curl -s -X POST \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"invalid_payload"}' \
  "$REJECT_URL"
```

Moves the message to the dead-letter queue. Use for: schema validation failures, unknown event types, permanently malformed data.

### Decision guide

| Scenario | Action |
|---|---|
| Processing succeeded | `ack` |
| Temporary failure (network, timeout, rate limit) | `nack` (will retry with backoff) |
| Permanent failure (bad data, unknown event) | `reject` (moves to dead-letter) |
| Unsure if transient or permanent | `nack` (safer — allows retry) |

---

## Complete code examples

### Bash — REST polling

```bash
#!/usr/bin/env bash
set -euo pipefail

NEXT_URL="${HOOQUE_QUEUE_NEXT_URL:-https://app.hooque.io/queues/<consumerId>/next}"
TOKEN="${HOOQUE_TOKEN:-hq_tok_replace_me}"

while true; do
  tmp_headers="$(mktemp)"
  body="$(curl -sS -D "$tmp_headers" -H "Authorization: Bearer $TOKEN" "$NEXT_URL" || true)"
  status="$(awk 'NR==1 {print $2}' "$tmp_headers" | tr -d '\r')"

  if [[ "$status" == "204" ]]; then
    rm -f "$tmp_headers"; sleep 1; continue
  fi
  if [[ -z "$status" || "$status" -ge 400 ]]; then
    echo "next() failed: ${status:-unknown}" >&2
    rm -f "$tmp_headers"; sleep 2; continue
  fi

  meta="$(awk -F': ' 'tolower($1)=="x-hooque-meta" {sub(/\r$/,"",$2); print $2}' "$tmp_headers")"
  rm -f "$tmp_headers"

  # Process your event here
  echo "Processing event: $(echo "$meta" | jq -r '.messageId')"

  # Ack on success
  ack_url="$(echo "$meta" | jq -r '.ackUrl // empty')"
  if [[ -n "$ack_url" ]]; then
    curl -sS -X POST -H "Authorization: Bearer $TOKEN" "$ack_url" >/dev/null
  fi
done
```

### Node.js — REST polling

```javascript
// Node 18+ (fetch built-in)
const NEXT_URL = process.env.HOOQUE_QUEUE_NEXT_URL ?? "https://app.hooque.io/queues/<consumerId>/next";
const TOKEN = process.env.HOOQUE_TOKEN ?? "hq_tok_replace_me";
const headers = { Authorization: `Bearer ${TOKEN}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function main() {
  while (true) {
    const resp = await fetch(NEXT_URL, { headers });
    if (resp.status === 204) { await sleep(1000); continue; }
    if (!resp.ok) { console.error("next() failed:", resp.status); await sleep(2000); continue; }

    const meta = JSON.parse(resp.headers.get("X-Hooque-Meta") ?? "{}");
    const raw = await resp.text();
    const payload = (resp.headers.get("content-type") ?? "").includes("json") ? JSON.parse(raw) : raw;

    try {
      console.log("Processing:", meta.messageId);
      // ... your business logic here ...
      if (meta.ackUrl) await fetch(meta.ackUrl, { method: "POST", headers });
    } catch (err) {
      const url = meta.nackUrl ?? meta.rejectUrl;
      if (url) await fetch(url, {
        method: "POST",
        headers: { ...headers, "Content-Type": "application/json" },
        body: JSON.stringify({ reason: err.message }),
      });
    }
  }
}

main();
```

### Python — REST polling

```python
# Python 3.11+ (requests)
import json, os, time
import requests

NEXT_URL = os.getenv("HOOQUE_QUEUE_NEXT_URL", "https://app.hooque.io/queues/<consumerId>/next")
TOKEN = os.getenv("HOOQUE_TOKEN", "hq_tok_replace_me")

with requests.Session() as s:
    s.headers.update({"Authorization": f"Bearer {TOKEN}"})

    while True:
        resp = s.get(NEXT_URL, timeout=30)
        if resp.status_code == 204:
            time.sleep(1.0); continue
        if resp.status_code >= 400:
            print("next() failed:", resp.status_code); time.sleep(2.0); continue

        meta = json.loads(resp.headers.get("X-Hooque-Meta", "{}"))
        payload = resp.json() if "json" in resp.headers.get("content-type", "") else resp.text

        try:
            print("Processing:", meta.get("messageId"))
            # ... your business logic here ...
            if meta.get("ackUrl"):
                s.post(meta["ackUrl"], timeout=30)
        except Exception as e:
            url = meta.get("nackUrl") or meta.get("rejectUrl")
            if url:
                s.post(url, json={"reason": str(e)}, timeout=30)
```

---

## Worker behavior rules

1. **Never ack before business logic succeeds.** Only call `ackUrl` after your processing is complete.
2. **Distinguish transient vs permanent failures.** Use `nack` for retryable errors, `reject` for non-retryable ones.
3. **Keep handlers idempotent.** Messages may be redelivered — ensure safe-to-replay processing.
4. **Log `messageId` / `deliveryId`** for traceability and debugging.
5. **Backoff on empty polls.** When REST returns `204`, wait 1–5 seconds before polling again.
6. **Reconnect SSE on disconnect.** SSE connections may drop — reconnect with exponential backoff.
7. **Handle encoding correctly** for SSE. Check the `encoding` field — if `"base64"`, decode the payload before processing.

## Error handling and retry strategy

**Network errors (consumer → Hooque)**
- If `GET /next` or the SSE connection fails, back off and retry (start at 1s, double up to 30s, reset on success).
- Lifecycle calls (`ack`/`nack`/`reject`) should be retried on network failure — the message will be redelivered if not acked within the lease timeout.

**Application errors (your processing logic)**
- Transient errors (HTTP 429, 502, 503, timeouts, connection refused) → `nack` with reason.
- Permanent errors (400, 404, validation failure, unknown event type) → `reject` with reason.
- Unknown errors → `nack` (safer — allows retry; after max retries the message will be dead-lettered automatically).

**Lease timeout**
- Each message has a lease. If you don't call `ack`/`nack`/`reject` before the lease expires, the message is automatically redelivered. This is a safety net, not a normal flow — always explicitly call a lifecycle action.

## Creating webhooks via API

These operations are typically done via the Hooque dashboard at `https://app.hooque.io`, but can also be done via API.

### Create webhook endpoint

```bash
curl -s -X POST \
  -H "Authorization: Bearer $HOOQUE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-webhook","generateToken":true}' \
  "https://app.hooque.io/api/webhooks"
```

The response includes:
- The **webhook URL** to give to the provider.
- If `generateToken: true`, a new **API token** for the auto-created queue consumer.

### Provider-specific configuration

When creating a webhook, you can specify `providerConfig` for built-in signature verification:

```json
{"name": "stripe-wh", "generateToken": true, "providerConfig": {"type": "stripe"}}
```

Supported provider types: `github`, `stripe`, `shopify`, `slack`, `discord`, `svix`, `clerk`, `typeform`, `hubspot`, `workos`, `lemon_squeezy`, `intercom`, `pagerduty`, `jira`, `bitbucket`, `gitlab`, `asana`, `monday`, `sendgrid`, `twilio`, `mailgun`, `mailjet`.

For generic webhooks, use the `webhook` type with a specific auth method:

```json
{"providerConfig": {"type": "webhook", "auth_type": "hmac", "algorithm": "sha256", "encoding": "hex", "header": "X-Signature"}}
```

Available generic auth types: `hmac` (with algorithm/encoding/header), `basic` (with username), `api_key` (with header), `query_param` (with paramName).

### OpenAPI spec

Full API documentation: `https://app.hooque.io/api/docs/openapi.json`

## Common gotchas

1. **Don't forget the `X-Hooque-Meta` header in REST.** The payload is in the body but the metadata (including lifecycle URLs) is in this header, not mixed into the body.
2. **SSE payloads may be base64-encoded.** Always check the `encoding` field in the SSE envelope before parsing.
3. **Lifecycle URLs are one-time-use.** Calling `ackUrl` twice on the same message will fail on the second call.
4. **Tokens are per-consumer, not per-webhook.** A webhook can have multiple queue consumers, each with their own token.
5. **REST 204 means empty queue, not an error.** Back off and poll again — don't treat it as a failure.
6. **SSE heartbeats are comments.** Lines starting with `:` are keepalives — skip them when parsing events.

## Canonical links

- Home: https://hooque.io/
- Sign up: https://hooque.io/signup/
- Dashboard / App: https://app.hooque.io/
- Documentation: https://docs.hooque.io/
- OpenAPI schema: https://app.hooque.io/api/docs/openapi.json
- Guides: https://hooque.io/guides/
- Receive webhooks guide hub: https://hooque.io/guides/receive-webhooks/
- Use cases: https://hooque.io/use-cases/
- Pricing: https://hooque.io/pricing/
- **Agent briefing (positioning)**: https://hooque.io/agents.md
