StrataDesk / Docs / Integrations / Recurring plan callbacks

Recurring plan callbacks

How to tell StrataDesk that the work on a plan-generated ticket finished — or failed.

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

  1. Open the plan in StrataDesk: Recurring plans → click the plan name.
  2. In the Integration tokens card, click New token.
  3. Give it a meaningful name ("prod-backup-script").
  4. 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