NQD API integrations and webhooks
Manage API keys, signed webhooks, delivery attempts, and activity logs from Dash or the workspace-scoped API.
The same resources are available in Dash under Settings → Integrations and through the API.
Create an API key
Sign in to Dash, open Settings → Integrations → API keys, choose Create API key, enter a name, and select the scopes the integration needs. The secret is shown once. Copy it into your secret manager immediately; NQD stores only the verifier and never displays the secret again. Revoke the key from the same page when it is no longer needed.
The API equivalent uses a normal bearer credential and an explicit workspace header:
curl "$API/v1/integrations/api-keys" \
-H "Authorization: Bearer $TOKEN" \
-H "Workspace-Id: $WORKSPACE" \
-H "Content-Type: application/json" \
--json '{
"name": "warehouse sync",
"scopes": ["invoices.list"]
}'The request contains one API key definition. The response places metadata and a one-time secret in data. Use the secret as the bearer credential. Lists expose scopes, last-use time, and revocation state, never the secret. DELETE /v1/integrations/api-keys/{apiKeyId} revokes a key.
Webhooks
Create a webhook from Settings → Integrations → Webhooks or POST /v1/webhooks with a name, HTTPS destination, and event names. A new webhook starts as pending_verification.
Call POST /v1/webhooks/{webhookId}/verify after confirming the destination. An active webhook signs each delivery with the one-time signingSecret returned at creation. Verify webhook-id, webhook-timestamp, and webhook-signature before processing a payload. Use the test action to validate a receiver without authoring a financial record.
REST returns the signing secret once when a webhook is created or rotated, with caching disabled. CLI and MCP redact it by default. Pass --reveal-secrets to nqd webhooks create or nqd webhooks <id> rotate, or set revealSecrets: true on the matching MCP tool, to receive that one-time value. Save it directly in your receiver’s secret manager. Webhook lists, logs, errors, and local workspace state never expose it.
Rotation preserves a verified webhook’s active or paused state. overlapSeconds accepts 60 through 86,400 seconds and defaults to 86,400. For example, nqd webhooks WEBHOOK_ID rotate --overlap-seconds 3600 --reveal-secrets gives an hour to update the receiver. During overlap, webhook-signature contains space-separated v1,<signature> values from the new and previous secrets. After expiry, sends and retries use only the new secret. A second rotation during an active overlap returns a conflict; wait for expiry before rotating again. A pending destination stays pending, and changing the destination clears the overlap and requires verification. NQD stores both secrets encrypted and never returns the previous secret.
Delivery states are pending, delivering, unknown, succeeded, and failed. An unknown result means that NQD cannot confirm the receiver’s outcome. Show it as Unconfirmed. Each attempt records its status, category, HTTP status, latency, and a response excerpt limited to 512 bytes. Unknown attempts become manually retryable after five minutes; fresh unknown attempts cannot be retried before then, and succeeded deliveries cannot be retried. Retry one with POST /v1/webhooks/{webhookId}/deliveries/{logId}/retry; pause or resume a webhook while investigating failures.
The event body is a CloudEvents-shaped JSON object. It contains the event ID, action, workspace ID, record ID, actor ID, operation ID, and occurrence time. NQD signs the exact raw body. Verify webhook-id, webhook-timestamp, and webhook-signature before processing the event. Deduplicate on webhook-id, because a receiver can see more than one delivery attempt.
The signed body has this shape. actorId and operationId may be null when the committed mutation has no authenticated actor or operation identifier.
{
"specversion": "1.0",
"id": "log_01...",
"source": "/workspaces/workspace_01...",
"type": "invoices.upsert",
"subject": "invoice_01...",
"time": "2026-10-10T12:00:00.000Z",
"datacontenttype": "application/json",
"dataschema": "/schemas/webhooks/invoices.upsert/1.0.0.json",
"data": {
"workspaceId": "workspace_01...",
"recordId": "invoice_01...",
"actorId": "profile_01...",
"operationId": "invoices.upsert"
}
}Logs
GET /v1/logs returns immutable workspace activity generated by successfully committed workspace mutations. Filter with filter[action]=eq.<action>, use limit up to 250, and continue with page.nextCursor. Validation failure, permission denial, or a rolled-back transaction creates no activity fact. Log responses contain fixed identity fields, not arbitrary metadata, request bodies, secrets, or raw credentials.
Core writes the activity fact and pending delivery in the same database transaction. Delivery starts after commit. A failed delivery never rolls back the domain mutation. The same durable worker runs after commit and on the private five-minute Netlify schedule. It retries network failures, timeouts, HTTP 5xx, and HTTP 408, 425, or 429, with increasing delays and jitter. A valid Retry-After is bounded to two hours. Redirects, other HTTP 4xx, unsafe destinations, TLS failures, and invalid configuration stop automatic retries. There are at most eight automatic attempts, including the initial send. Delivery lists expose nextAttemptAt and leaseExpiresAt; internal lease tokens stay private. Manual retries remain available for eligible pending, failed, or cooled-down unconfirmed deliveries.
Test deliveries create a webhooks.test activity and delivery row for the selected webhook, without creating an unrelated financial or domain mutation. Webhook signatures are base64 HMAC-SHA256 values over the exact raw body.
Pausing a verified webhook keeps matching committed events queued. Resume wakes the bounded worker and replays them in event order; repeated resume does not duplicate queued attempts. Unverified destinations cannot be paused or resumed into service. Changing the destination requires verification again and marks older unresolved deliveries as stale_generation, so their events cannot reach the new endpoint. Their attempt history remains available. Editing a name or keeping the same destination preserves verification. A request already in flight to the old destination may finish.
All integration routes require a valid bearer credential, the selected Workspace-Id, and the caller’s workspace settings permission where the operation changes configuration.
Configure integration retention
Retention controls apply to one workspace. Cleanup is disabled by default. Set enabled to true to allow automatic cleanup. Set either retention window to null to keep that record type forever. A configured window accepts an integer
from 1 through 3650 days.
The REST operations use these paths:
| Operation | Request |
|---|---|
| Read policy | GET /v1/retention |
| Replace policy | PUT /v1/retention |
| Preview cleanup | GET /v1/retention/preview |
| Prune eligible rows | POST /v1/retention/prune |
All four operations require the selected Workspace-Id and the caller’s
workspace settings permission. retention.prune is destructive in the
operation catalog and MCP annotations.
Update the policy with a complete body:
curl "$API/v1/retention" \
-X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Workspace-Id: $WORKSPACE" \
-H "Content-Type: application/json" \
--json '{
"enabled": true,
"logRetentionDays": 90,
"deliveryRetentionDays": 30
}'Preview before pruning. The response includes policyRevision, candidate
counts, protected counts, protected reasons, and the oldest candidate times.
Use the returned revision in the prune request:
curl "$API/v1/retention/preview" \
-H "Authorization: Bearer $TOKEN" \
-H "Workspace-Id: $WORKSPACE"
curl "$API/v1/retention/prune" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Workspace-Id: $WORKSPACE" \
-H "Content-Type: application/json" \
--json '{
"policyRevision": "<policyRevision from preview>",
"maxRows": 100,
"confirm": "prune"
}'maxRows is optional and accepts an integer from 1 through 1000. The revision
and confirmation prevent a prune against a policy that changed after preview.
Prune removes eligible terminal delivery rows before unreferenced logs. It keeps pending, delivering, and unknown deliveries, failed deliveries with a current scheduled retry, paused webhook backlog, in-flight work, and logs that any delivery still references. Financial records are outside this policy and are never removed.
Automatic maintenance is bounded and visits enabled workspaces fairly. It does not delete rows for disabled policies or null windows. Expired previous webhook secrets use an independent cleanup pass, so secret cleanup does not depend on retention being enabled.
The same operations are available as native CLI commands:
nqd integrations retention get
nqd integrations retention update --body '{"enabled":true,"logRetentionDays":90,"deliveryRetentionDays":30}'
nqd integrations retention preview
nqd integrations retention prune --body '{"policyRevision":"<policyRevision from preview>","maxRows":100,"confirm":"prune"}'MCP exposes the operation IDs retention.get, retention.update, retention.preview, and retention.prune. Set workspaceId in each call.
Pass the update or prune fields in the matching tool input. The prune tool
requires the same policyRevision and confirm values as REST, with optional maxRows.