Recurring plan callbacks
How to tell StrataDesk that the work on a plan-generated ticket finished — or failed.
On this page
Overview
Each recurring plan generates one parent ticket plus one child
per matched asset on every fire. The child sits on the
await_cb
workflow step waiting for your script to confirm.
Your integration POSTs a signed JSON body to
POST /api/v1/integrations/callback.
StrataDesk verifies the bearer + signature, marks the ticket
resolved_cb
or
missed_cb
depending on the event, and stores your result fields on the
ticket for the monthly report.
Mint a token
- Open the plan in StrataDesk: Recurring plans → click the plan name.
- In the Integration tokens card, click New token.
- Give it a meaningful name ("prod-backup-script").
- Copy both the bearer token and the HMAC secret now. They're shown exactly once. If you lose either, you'll need to revoke and mint a new pair.
Environment variables
Stash both secrets in your script's secret store / env. Never hardcode them.
# StrataDesk callback credentials (per-plan)
export STRATADESK_URL='https://your-tenant.stratadesk.io'
export STRATADESK_TENANT='your-tenant-code' # inombra, acme, etc.
export STRATADESK_TOKEN='sdk_live_…' # Authorization bearer
export STRATADESK_HMAC_SECRET='64-char-hex…' # X-Signature key
Signing the request
Every callback request carries four headers:
Authorization: Bearer <token>— proves who's calling.X-Timestamp: <unix-seconds>— replay protection. StrataDesk rejects anything outside ±5 minutes.X-Signature: <hex>— HMAC-SHA256 over"<timestamp>.<raw-body>".X-Idempotency-Key: <uuid>— re-posts with the same key return the cached response.
The signature is computed against the raw bytes of the request body. If your HTTP client pretty-prints or reformats the JSON between signing and sending, the signature won't verify.
Bash example
#!/usr/bin/env bash
# Reports execution.succeeded against a ticket in a recurring plan.
# Required env: STRATADESK_URL, STRATADESK_TENANT, STRATADESK_TOKEN,
# STRATADESK_HMAC_SECRET
set -euo pipefail
TICKET_ID="${1:?usage: report-success.sh <ticket-id>}"
DURATION_SECONDS="${2:-0}"
TS=$(date +%s)
BODY=$(jq -cn \
--arg tid "$TICKET_ID" \
--argjson dur "$DURATION_SECONDS" \
'{ticket_id:$tid, event:"execution.succeeded", result:{duration_seconds:$dur}}')
SIG=$(printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$STRATADESK_HMAC_SECRET" -hex \
| awk '{print $2}')
IDEM=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)
curl --fail-with-body -sS -X POST \
"$STRATADESK_URL/api/v1/integrations/callback" \
-H "Authorization: Bearer $STRATADESK_TOKEN" \
-H "X-Client-ID: $STRATADESK_TENANT" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-H "X-Idempotency-Key: $IDEM" \
-H "Content-Type: application/json" \
--data "$BODY"
For a failure case, swap
"event":"execution.succeeded"
for
"event":"execution.failed"
and add an error
string to the result object.
Python example
import hashlib, hmac, json, os, time, uuid
import requests
def report_callback(ticket_id: str, event: str, result: dict) -> None:
base = os.environ["STRATADESK_URL"]
token = os.environ["STRATADESK_TOKEN"]
secret = os.environ["STRATADESK_HMAC_SECRET"].encode()
tenant = os.environ["STRATADESK_TENANT"]
body = json.dumps(
{"ticket_id": ticket_id, "event": event, "result": result},
separators=(",", ":"),
)
ts = str(int(time.time()))
signature = hmac.new(
secret, f"{ts}.{body}".encode(), hashlib.sha256,
).hexdigest()
resp = requests.post(
f"{base}/api/v1/integrations/callback",
headers={
"Authorization": f"Bearer {token}",
"X-Client-ID": tenant,
"X-Timestamp": ts,
"X-Signature": signature,
"X-Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
data=body,
timeout=10,
)
resp.raise_for_status()
Idempotency & retries
Callbacks are processed exactly once per
(ticket_id, X-Idempotency-Key)
pair. A retry with the same key returns the cached response
body and never re-runs the side effects (counters,
transitions, audit). Generate one fresh UUID per logical
callback attempt; if your network fails mid-flight, retry
with the same key.
On the server side, dispatch retries follow this backoff
schedule on 5xx / timeout responses:
1s, 10s, 60s, 300s, 1800s
(≈35 minutes total horizon). 4xx responses are terminal — the
request stops being retried because client errors don't get
better with time.
Troubleshooting
401 — invalid signature
Your computed signature doesn't match what the server derives. Most common causes: signed body bytes ≠ sent body bytes (pretty-printing, charset), or the HMAC secret has been rotated.
401 — timestamp stale / future
X-Timestamp is outside the ±5-minute
window. Check clock drift on the host running your
script. NTP solves this.
401 — token invalid / revoked
Token was deleted in the UI or rotated. Mint a new one, update the env var, retry.
404 — ticket not found
The ticket_id doesn't belong to this
plan. Plans are tenant-isolated — make sure the
ticket's source plan matches the plan whose token
you're using.
Ticket goes to missed even though you called back
Check the plan's grace period: the callback
has to land before scheduled_for + grace.
If your work takes longer than the grace, widen it on
the plan.
Still stuck?
Contact support