Developer documentation

Send events to Doqet, receive webhooks and verify that they came from us.

Quick start

Your systems send events to one HTTPS endpoint per source. Rules and destinations are set up in the console.

  • Create a source and copy its token. The token is shown only once.
  • Add a destination, then a rule that routes events to it.
  • Send an event to the source's ingest endpoint. The Sources page in the console shows its address, with a copy button. The example reads that address from DOQET_INGEST_URL.
curl -X POST "$DOQET_INGEST_URL" \
  -H "Authorization: Bearer $DOQET_SOURCE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8841-paid" \
  -d '{
    "type": "order.paid",
    "title": "Order 8841 paid",
    "priority": "high",
    "labels": { "region": "eu" },
    "data": { "order_id": "8841", "amount": 299 }
  }'

Send an event

Send POST /v1/ingest/{source_id} with a JSON body and Authorization: Bearer <source token>. A 202 Accepted response means the event is stored.

FieldTypeDefaultNotes
typestringgenericUp to 200 bytes. Any name you choose.
titlestringUp to 1,000 bytes.
bodystringCounts toward the total event size.
prioritystringnormalUp to 100 bytes. Any value your rules can match.
labelsobject{}Free-form values for rules, such as labels.environment.
dataobject{}Free-form values for rules, such as data.order_id.
dedup_keystringUp to 512 bytes. Identifies repeats for rules that deduplicate.
expires_atRFC 3339 time24 hours after receiptIn the future and at most 30 days ahead.

An event needs a title, a body or a non-empty data object. Unknown fields are rejected, and numbers keep their exact value.

Response

The response has the event's id, its status and whether it was a duplicate. A new event's status is one of pending (queued for delivery), batched (waiting in an aggregation window), unmatched (no rule applied) or suppressed (a rule treated it as a repeat).

Idempotency

Send an Idempotency-Key header so that retrying a request is safe. Use a value of up to 255 bytes that identifies the event in your system, such as order-8841-paid.

  • The same key and body within 24 hours returns 202 with the original event's id and duplicate: true. Nothing is queued again.
  • The same key with a different body returns 409 idempotency_key_reused.
  • Keys belong to one source, so another source can use the same key.
  • After 24 hours the key is forgotten and the request creates a new event.

Idempotency-Key protects ingestion from your own retries. A rule's deduplication is separate: it suppresses repeat events on purpose, within a window you choose.

Rate and size limits

LimitValueWhen exceeded
Request body64 KiB413 payload_too_large
Stored event after normalization128 KiB413 payload_too_large
Each number1,024 digits, exponent from -1024 to 1024400 invalid_request
Events per minute, per workspaceSet by your plan429 quota_exceeded with Retry-After
Retained events and storageSet by your plan429 quota_exceeded

Settings, then Usage, shows your workspace's current limits. A rejected request stores nothing and doesn't count toward usage.

Error codes

Errors share one JSON shape. code is stable and safe to branch on. message is English text for people and can change.

Response
{
  "error": {
    "code": "quota_exceeded",
    "message": "Workspace capacity reached: events_per_minute",
    "limit": "events_per_minute"
  }
}
StatusCodeMeaning
400invalid_requestMalformed JSON, an unknown field or a number out of range.
400event_content_requiredThe event has no title, body or data.
400event_field_too_longtitle, type, priority or dedup_key is over its limit.
400event_expiry_invalidexpires_at isn't within the next 30 days.
400idempotency_key_too_longIdempotency-Key is over 255 bytes.
401unauthorizedThe token doesn't match this source.
403source_disabledThe source is disabled or archived.
409idempotency_key_reusedThe key was already used with different content.
413payload_too_largeThe request or the stored event is over its size limit.
429quota_exceededA workspace limit was reached. limit names it.
500internal_errorSomething failed on our side. Retry with the same Idempotency-Key.

Retry 429 and 5xx responses and network errors with the same Idempotency-Key. Wait for Retry-After when it is present. Fix other 4xx responses before sending again.

Webhook payloads

A webhook destination receives one POST for each delivery. The endpoint must be HTTPS on a public address. Doqet doesn't follow redirects. It waits up to 10 seconds for your response headers and 15 seconds for the whole request.

Single event

The body is your event plus its id, source_id and created_at.

Body
{
  "id": "evt_5a1e7c3f9b2d4e68",
  "source_id": "src_8d2b6f4a1c7e4093",
  "type": "order.paid",
  "title": "Order 8841 paid",
  "body": "",
  "priority": "high",
  "labels": { "region": "eu" },
  "data": { "order_id": "8841", "amount": 299 },
  "created_at": "2026-11-01T09:30:00Z",
  "expires_at": "2026-11-02T09:30:00Z"
}

Batch

A rule with an aggregation window sends one request for each group. Each entry in events has the same fields as a single event. One request holds at most 100 events.

Body
{
  "rule_id": "rul_3f0c1a9e7b5d4c26",
  "count": 2,
  "events": [
    { "id": "evt_5a1e7c3f9b2d4e68", "type": "server.disk.warning", "title": "Disk at 91%" },
    { "id": "evt_7b3d9e1f2c4a6058", "type": "server.disk.warning", "title": "Disk at 94%" }
  ]
}

Your response

Any 2xx response counts as delivered and the response body is ignored. The body is fixed at the first attempt, so every retry sends the same bytes.

Webhook headers

HeaderValue
Content-Typeapplication/json
User-AgentDoqet/ followed by the release version, for example Doqet/1.0.0.
X-Doqet-Delivery-IDIdentifies this delivery and stays the same across retries.
X-Doqet-Event-IDThe event. For a batch, the first event.
X-Doqet-AttemptThe attempt number, starting at 1.
X-Doqet-TimestampUnix time in seconds when this attempt was sent.
X-Doqet-Signaturev1= and a hex HMAC. Sent for the JSON format when the destination has a signing secret.
X-Doqet-Testtrue on a test message sent from the console. Real deliveries never carry it.
AuthorizationBearer <token>. Sent when the destination has a bearer token.

Verify signatures

Set a signing secret on a webhook destination and every request carries X-Doqet-Signature. Verify it before you trust a request.

  • Read the raw request body as bytes. Parsing and re-serializing JSON changes them.
  • Join X-Doqet-Timestamp, a period, X-Doqet-Delivery-ID, a period and the raw body.
  • Compute HMAC-SHA256 of that value with your signing secret, as lowercase hex, and prefix v1=.
  • Compare the result with X-Doqet-Signature in constant time.
  • Reject old timestamps. Each attempt is signed with its own timestamp, so a five-minute window doesn't block retries.
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

// rawBody must be the exact bytes received, before any JSON parsing.
export function verifyDoqetSignature(
  { secret, timestamp, deliveryId, signature, rawBody },
  now = Date.now() / 1000,
) {
  if (!/^[0-9]+$/.test(timestamp ?? "")) return false;
  if (Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected =
    "v1=" +
    createHmac("sha256", secret)
      .update(`${timestamp}.${deliveryId}.`)
      .update(rawBody)
      .digest("hex");
  const given = Buffer.from(signature ?? "");
  return (
    given.length === expected.length &&
    timingSafeEqual(given, Buffer.from(expected))
  );
}

Retries and expiry

Doqet retries a delivery automatically when it could still succeed. Delays grow with each attempt, from 10 seconds to 8 hours, and include random jitter.

ResultWhat happens
2xxDelivered.
Network error or timeoutRetried.
408, 429 or 5xxRetried. Retry-After is honoured, up to 6 hours.
Any other statusFailed, with no automatic retry.

By default a delivery gets up to 10 attempts. The deployment sets that limit, not each destination. Those attempts span about 16 to 20 hours. After that it is marked failed, and you can retry it from the event's page until it expires.

Expiry

Every event expires, 24 hours after receipt by default or at expires_at. No attempt starts after expiry, and a retry that would fall after it isn't scheduled. Set expires_at to match how long the notification stays useful. A batch expires with its earliest event.

At-least-once delivery

Delivery is at-least-once. There is no exactly-once guarantee between Doqet and your endpoint.

A repeat happens when Doqet can't tell whether a request worked. For example, your endpoint may process an event and lose the connection before its response arrives, so Doqet sends the same body again.

Make your receiver idempotent

  • Store each X-Doqet-Delivery-ID you process and skip any you have already seen.
  • Record the ID in the same transaction as your side effect, so a crash can't separate them.
  • Respond with 2xx quickly and do slow work in the background. If your response headers don't arrive within 10 seconds, the attempt counts as a timeout and is retried.
  • Don't rely on arrival order. Use created_at when order matters.

Integration API

The integration API manages one workspace with an API key instead of a browser session, so scripts and agents can set up routing and read what happened. Send requests to /api/integration/<route> with Authorization: Bearer dqk_....

  • Owners and administrators create and revoke keys under Integration in the console. A workspace can have up to 20, on every plan. A key is shown once.
  • A read key sees the overview, sources, destinations, rules, events and their deliveries, the inbox, usage, billing and the audit log, and can preview rules. Webhook URLs show only their host, as in https://example.com/***.
  • A write key can also create, change and archive sources, destinations and rules, rotate source tokens, send events and test messages, and retry deliveries.
  • A key never has more access than the member who created it, and it stops working if they leave the workspace.
  • Members, billing changes, retention settings and key management stay in the console.
curl "https://doqet.app/api/integration/sources" \
  -H "Authorization: Bearer $DOQET_API_KEY"

An API key is not a source token. A source token only sends events to its own source's ingest address, and an API key doesn't work there.

The full reference is plain Markdown for scripts and agents, and covers everything an integration needs. The Claude Code skill operates a workspace for you with the same API.

Developer documentation · Doqet