API

Everything the UI does goes through a JSON API, documented with Swagger at /docs on your instance (for example http://localhost:5252/docs). The raw OpenAPI spec is at /docs/json.

  1. Ingest endpoint
  2. Authentication
  3. Endpoints
  4. Examples

Ingest endpoint

This is the URL you give to other services.

POST /h/{key}
PUT  /h/{key}

The body must be JSON (objects, arrays or any JSON value), at most 1 MB. The Content-Type header isn’t checked strictly: a body that parses as JSON is accepted even when sent as text/plain.

Response When Logged?
202 {"id":"…","status":"queued"} Accepted and queued yes (pending)
400 Empty body, or invalid JSON sent as JSON yes (rejected)
401 The relay requires credentials and they’re missing or wrong (see Security) yes (rejected)
403 The relay is disabled yes (rejected)
404 No relay has this key (wrong or rotated URL) no
413 Body larger than 1 MB yes (rejected)
415 Body isn’t JSON yes (rejected)
429 More than 120 requests in a minute for this relay no

202 means stored, not delivered. Delivery happens in the background; see Logs, retries and the queue.

curl -i -X POST 'https://webhooks.example.com/h/vikunja-tasks_k3J9xQp2LmT8aB4cD6eF8gH0' \
  -H 'Content-Type: application/json' \
  -d '{"event_name":"task.created","data":{"task":{"title":"Fix login bug"}}}'

Authentication

The dashboard API (/api/…) uses a session cookie, send_any_webhook_session, set by logging in. From a script:

# log in and store the cookie
curl -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"…"}' \
  https://webhooks.example.com/api/auth/login

# use it
curl -b cookies.txt https://webhooks.example.com/api/relays

With AUTH_DISABLED=true no cookie is needed.

In Swagger UI, log in through the /api/auth/login operation (“Try it out”). The browser keeps the cookie for the other calls.

Endpoints

Group Endpoints
System GET /healthz, GET /api/config
Auth POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me
Discord webhooks GET/POST /api/discord-webhooks, PATCH/DELETE /api/discord-webhooks/{id}, POST /api/discord-webhooks/{id}/test
Relays GET/POST /api/relays, GET/PATCH/DELETE /api/relays/{id}, POST /api/relays/{id}/rotate-key, POST /api/relays/{id}/auth/rotate, PUT /api/relays/{id}/sample, POST /api/relays/{id}/capture, POST /api/relays/{id}/test
Templates GET/POST /api/relays/{id}/templates, PATCH/DELETE /api/relays/{id}/templates/{templateId}, POST /api/preview, POST /api/mapping/auto
Queue and logs GET /api/queue, GET /api/queue/stats, GET /api/queue/{id}, POST /api/queue/{id}/send, POST /api/queue/send, POST /api/queue/send-oldest, POST /api/queue/{id}/retry, POST /api/queue/retry, POST /api/queue/{id}/cancel, POST /api/queue/cancel, POST /api/queue/{id}/replay

Swagger has the request and response schemas for each.

Examples

Save a Discord webhook, then create a relay that uses it:

curl -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"name":"#ci","url":"https://discord.com/api/webhooks/…"}' \
  https://webhooks.example.com/api/discord-webhooks
# → {"id":"<webhook-id>", …}

curl -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"name":"CI","discordWebhookId":"<webhook-id>"}' \
  https://webhooks.example.com/api/relays

Or pass discordWebhookUrl (and optionally discordWebhookName) instead of discordWebhookId. The URL is saved, or matched to one you already saved.

Relays also take maxAttempts (1–20, default 5) and auth, for example {"type":"token"}, {"type":"basic","username":"svc"} or {"type":"hmac","headerName":"X-Hub-Signature-256","prefix":"sha256=","secret":"…"}. Omitted secrets are generated; GET /api/relays/{id} returns them to the owner (the list endpoint never does).

Find failed deliveries from the last day that Discord rate-limited:

curl -b cookies.txt -G https://webhooks.example.com/api/queue \
  --data-urlencode 'status=failed' --data-urlencode 'status=dead' \
  --data-urlencode 'statusCode=429' \
  --data-urlencode "from=$(date -u -v-1d +%FT%TZ 2>/dev/null || date -u -d '1 day ago' +%FT%TZ)" \
  --data-urlencode 'sort=receivedAt:desc'

Query parameters for GET /api/queue: relayId (repeatable), status (repeatable), from, to (ISO dates), statusCode, hasError, q (text search), path + pathValue (payload field equals), sort (receivedAt|attempts|lastStatusCode|nextAttemptAt|deliveredAt|status|relayName + :asc|:desc), page, pageSize (max 200).

Retry everything that died for one relay:

curl -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"filter":{"relayId":["<relay-id>"],"status":["dead"]}}' \
  https://webhooks.example.com/api/queue/retry