Administration

Integrations: API tokens and webhooks

Connect other systems: create and read tickets through the API, receive signed events through webhooks.

The Integrations page connects RANCAQ to other systems in your organization, such as monitoring tools, HR systems, team chat, or automation like n8n. You find it under Administration → Integrations, and it has two parts:

  • API tokens: other systems create and read tickets through the REST API, without a person's account.
  • Webhooks: RANCAQ sends ticket events to an HTTPS address of yours as they happen.

Who can manage it

Managing tokens and webhooks requires the Manage integrations permission. By default only Tenant Admin has it, because a token can open tickets to another system. Auditors can view tokens, webhooks, and delivery history without changing anything. See Users, teams, and roles.

API tokens

Creating a token

  1. Open Administration → Integrations, tab API tokens, and click New token.
  2. Enter a Name that says who uses it, e.g. "Head office Zabbix".
  3. Choose the Permissions. Pick as narrowly as you can:
PermissionMeaning
Read tickets created by this integrationOnly tickets this token created itself.
Read all workspace ticketsEvery ticket, without internal notes. Choose it only when the receiving system really needs to see everything.
Create ticketsNew tickets with channel API, reported by the integration identity.
Write public commentsPublic comments on tickets the token can read. Needs one of the read permissions.
Create on behalf of another requesterSpecial permission. The ticket is recorded on behalf of an active member of this workspace, and the audit records both the token and the requester represented. Needs Create tickets.
  1. Choose the Expiry and click Create token.
  2. Copy the token shown (rqa_…) into the secret store of the receiving system. The token is shown only once. If you lose it, revoke it and create a new one.

A workspace can have at most 10 active tokens.

Integration identity

Each token has its own identity named Integrasi: . That name appears as the actor in the ticket timeline, comments, and Audit, so you always know which integration acted. This identity:

  • cannot sign in to the application and receives no email;
  • does not appear in the user list and does not count as an agent seat;
  • cannot be given a role or added to a team.

Using a token

Send the token in the Authorization header:

curl -s https://app.rancaq.com/api/v1/tickets?limit=20 \
  -H "Authorization: Bearer rqa_…"

Endpoints that accept a token:

EndpointPermission
GET /api/v1/tickets and GET /api/v1/tickets/{id}Read
GET /api/v1/tickets/{id}/comments and …/activitiesRead
POST /api/v1/ticketsCreate tickets
POST /api/v1/tickets/{id}/commentsWrite public comments

Other endpoints reject tokens with 401.

Creating a ticket:

curl -s -X POST https://app.rancaq.com/api/v1/tickets \
  -H "Authorization: Bearer rqa_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: zabbix-event-48213" \
  -d '{"kind":"INCIDENT","subject":"DB server disk 95%","description":"From Zabbix","urgency":"HIGH"}'
  • Idempotency-Key: send a unique key per event in your system. A repeated request with the same key does not create a second ticket. Keys are scoped per token, so two integrations that happen to use the same key do not interfere.
  • On behalf of a requester: with the special permission, add "requested_for_email": "[email protected]" (or requested_for_id). That requester must be an active member of this workspace. They see the ticket in the portal and receive email as usual.
  • Comments: POST …/comments with {"body": "…"} is always public. A token can neither read nor write internal notes. Integration comments do not count as the team's first response and do not clear the Requester replied marker.

Each token is limited to 120 requests per minute. Beyond that the answer is 429 with a Retry-After header. A workspace whose trial has ended accepts read requests only.

Revoking a token

Click Revoke on the token's row. From then on every request with that token is rejected with 401, and this cannot be undone. The history of what the token did is kept.

Webhooks

Creating a webhook

  1. Open the Webhooks tab and click New webhook.
  2. Enter a Name and the receiver URL. The URL must be https:// and point to a public address. Private network addresses, loopback addresses such as 127.0.0.1, and special-purpose addresses are rejected. The check happens twice. On Save, RANCAQ rejects URLs that are private or special-purpose IP addresses and internal host names (for example *.local, *.internal). Ordinary host names are not looked up in DNS yet, because their address can change. On delivery, the host name is looked up: if it points to a private address, no connection is opened and the delivery is recorded as Failed in the history with the error ditolak: alamat tujuan tidak diizinkan … (destination address not allowed).
  3. Choose the Events:
EventWhen it is sent
ticket.created.v1A new ticket is created through any path.
ticket.status_changed.v1The ticket status changes, including automatic changes.
ticket.assigned.v1The ticket's team or assignee changes.
ticket.comment_added.v1A public comment is added. Internal notes are never sent.
sla.breached.v1A response or resolution SLA deadline is missed.

Events may be left empty. A webhook without events receives only deliveries from automation rules that choose it (automation.rule_triggered.v1). See Automation. The webhook card names the rules that send to it.

  1. Click Save, then copy the signing secret (whsec_…). The secret is shown only once. If you lose it, use Rotate secret.
  2. Click Send test on the webhook card to send ping.v1 to the receiver.

A workspace can have at most 10 active webhooks. A webhook only receives events that happen after it was created.

What is sent

RANCAQ sends a POST with JSON:

{
  "event_id": "5d0b…",
  "event_type": "ticket.status_changed.v1",
  "tenant_id": "…",
  "occurred_at": "2026-10-06T13:20:05Z",
  "actor": { "type": "user", "id": "…", "name": "Agus" },
  "subject": { "type": "ticket", "id": "…", "number": "INC-2026-000123" },
  "data": {
    "ticket": { "number": "INC-2026-000123", "subject": "VPN down", "status": "TRIAGED", "priority": "P2", "…": "…" },
    "change": { "from": "NEW", "to": "TRIAGED" }
  }
}
  • actor.type: user for a person, integration for an API token, system for RANCAQ's automatic changes.
  • data.ticket: the ticket's state when the event is processed, a few seconds after it happened.
  • Extra data per event: change for changes, comment for comments, sla for SLA breaches.

Headers sent along:

HeaderContent
X-ITSM-EventEvent type, same as event_type.
X-ITSM-Event-IDSame as event_id.
X-ITSM-Delivery-IDDelivery identifier, the same for every attempt and manual resend.
X-ITSM-Delivery-AttemptAttempt number: 1, 2, and so on.
X-ITSM-TimestampUnix seconds when the attempt was sent.
X-ITSM-Signaturesha256= followed by HMAC-SHA256.

Verifying the signature

Compute HMAC-SHA256 over <X-ITSM-Timestamp>.<raw body> with the full signing secret (including the whsec_ prefix) as the key. Then:

  1. compare the result with X-ITSM-Signature in constant time;
  2. reject timestamps more than 5 minutes away from your clock.

Compute over the raw body bytes, before parsing the JSON.

import crypto from 'node:crypto'

function valid(secret, timestamp, rawBody, signature) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(signature)
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
  return fresh && a.length === b.length && crypto.timingSafeEqual(a, b)
}
import hmac, hashlib, time

def valid(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
                                    hashlib.sha256).hexdigest()
    return abs(time.time() - int(timestamp)) < 300 and hmac.compare_digest(expected, signature)

To test your code: secret whsec_contoh, timestamp 1759750000, and body {"event_id":"x"} give sha256=b65486071794a9af8c19a14c7882303172b0131257324735b6abd3e060ddf454.

Duplicates and ordering

Delivery is at-least-once: an event can arrive more than once, e.g. when the receiver processed a delivery but its answer did not arrive before the timeout. To stay safe:

  • store the event_ids you have processed and ignore later deliveries with the same event_id;
  • answer quickly with a 2xx code and do the processing in the background;
  • do not rely on arrival order. Use occurred_at and the status in data.ticket.

Retries and history

A delivery succeeds when the receiver answers 2xx within 10 seconds. Redirects are not followed. On failure, RANCAQ tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours, with the same Delivery-ID and body. After six failed attempts the delivery is marked Failed.

The Delivery history tab shows every delivery with all its attempts: time, HTTP code, error message, and an excerpt of the receiver's answer. Filter by webhook, status, event, or ticket number. Resend sends a finished delivery once more, with the same Delivery-ID and event_id.

Automatic disabling

When 5 deliveries in a row end as Failed (each already tried six times), the webhook is disabled automatically. The webhook card shows the reason and time, and Audit records it. Fix the receiver, then Re-enable. Pending deliveries continue, and failed deliveries can be resent from the history.

A webhook failure never undoes a ticket change and never delays notification email.

See also

  • Audit log: the trail of token creation, revocation, and actions.
  • Ticket queue: tickets with channel API can be filtered in the queue.