Skip to main content
Was this helpful?

Webhook API

This is the technical reference for PrimeCal's incoming webhook receiver — the endpoint an external system calls to trigger a webhook.incoming automation rule. For a product walkthrough, see Webhooks in the User Guide.

Requests must be HMAC-signed — there is no unsigned mode

Every call to this endpoint is verified with an HMAC-SHA256 signature. There is no way to disable this per rule, and an unsigned request is rejected outright. Plan for signing from the start; a bare curl -d '{...}' POST will fail.

Endpoint​

POST https://api.primecal.eu/api/automation/webhook/{token}
GET https://api.primecal.eu/api/automation/webhook/{token}

{token} is unique per rule (webhookToken), found in the rule's Webhook tab in the app and regenerable from there.

  • POST — send your payload as a JSON body (Content-Type: application/json).
  • GET — for callers that can't send a POST body (IoT buttons, browser-triggered links). Query string parameters become the payload instead.

Both variants require the same signature.

Getting your signing secret​

Each rule has a webhookSecret, generated automatically when the rule is created. The web app does not currently display it — the only way to retrieve or rotate it is this endpoint:

POST /api/automation/rules/{id}/webhook/rotate-secret

Authenticated as the rule's owner (normal JWT auth, not the webhook token). Response:

{ "webhookSecret": "<new 128-char hex secret>", "graceUntil": "2026-09-26T12:00:00.000Z" }

Calling this rotates the secret — the previous secret keeps working until graceUntil (1 hour by default), then stops. There's no read-only "show current secret" call; if you've lost the secret, rotating is the only recovery.

Signing a request​

  1. Timestamp — current Unix time in seconds, as a string.
  2. Signed payload — "{timestamp}.{rawRequestBody}" (the exact bytes you send, not a re-serialized version — for GET requests, use an empty string in place of the body).
  3. Signature — HMAC-SHA256(webhookSecret, signedPayload), hex-encoded.

Send both as headers:

HeaderValue
X-PrimeCal-TimestampThe Unix timestamp (seconds) used above
X-PrimeCal-SignatureThe hex signature, optionally prefixed sha256=

(X-Webhook-Signature/X-Webhook-Timestamp and X-Hub-Signature-256 are also accepted, for compatibility with common webhook-sender conventions.)

Example (Node.js)​

const crypto = require('crypto');

const secret = 'the webhookSecret from rotate-secret';
const body = JSON.stringify({ event: 'order.completed', data: { order_id: '12345' } });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');

await fetch('https://api.primecal.eu/api/automation/webhook/YOUR_TOKEN', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PrimeCal-Timestamp': timestamp,
'X-PrimeCal-Signature': signature,
},
body,
});

Example (curl, precomputed)​

SECRET="the webhookSecret from rotate-secret"
BODY='{"event":"order.completed","data":{"order_id":"12345"}}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST "https://api.primecal.eu/api/automation/webhook/YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "X-PrimeCal-Timestamp: $TS" \
-H "X-PrimeCal-Signature: $SIG" \
-d "$BODY"

Timestamp tolerance and replay protection​

  • The timestamp must be within 5 minutes of the server's clock (WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS, default 300). Outside that window the request is rejected — this also means you can't replay an old captured request after the window closes.
  • Each (token, signature, timestamp) combination can only be used once. A byte-identical retry of the same signed request is rejected as a replay.

Rotating without downtime​

rotate-secret gives your new requests a new secret while the old one still verifies until graceUntil (default 1 hour). Update your external system's stored secret within that window; there's no way to extend it after the fact — rotate again if you need more time.

Rate limits​

Webhook calls are rate-limited per rule: 60 requests per 60-second window by default (AUTOMATION_WEBHOOK_MAX_PER_WINDOW / AUTOMATION_WEBHOOK_WINDOW_MS), independent of the general API rate limits in API Rate Limits. Exceeding it returns an error naming the retry delay.

Optional: restricting by source IP​

A rule can be configured with an IP allowlist (triggerConfig) so only requests from specific source IPs are accepted, on top of signature verification. This is set via the Automation API, not currently exposed in the rule builder UI.

Responses​

StatusMeaning
200{ "success": true, "ruleId": <id>, "message": "Webhook processed successfully" }
400Malformed signature format, or the rule isn't a webhook.incoming rule, or the rule is disabled
401Missing/invalid signature, timestamp outside tolerance, or a replayed request
403The rule has no webhookSecret at all (should not happen for a normally-created rule)
404Token doesn't match any rule
429Rate limit exceeded

A 200 means the request was accepted and evaluated — it does not mean the rule's conditions matched. Check the rule's audit log (Automation API) to see whether the action actually ran.