Back to site

Webhook Integrations

Send Tickory alerts to your own services, queues, bots, or automation tools. This guide covers setup, payload formats, signing headers, retries, and safe local testing.

Availability

Webhooks are available on non-free tiers, including beta. Exact limits depend on your current plan, so use the app UI as the source of truth for what your account can configure.

Setup

  1. Open a scan and go to Notifications.
  2. Paste the destination URL into Webhook URL.
  3. Choose a payload preset:
    • Default keeps the legacy Tickory payload.
    • OpenClaw preset sends the versioned contract envelope.
  4. Add custom headers if your receiver expects bearer tokens or API keys.
  5. Save the alert config, then use Test All or the webhook test action to verify delivery.

Custom headers are for outbound auth

Use custom headers such as Authorization or X-Api-Key when your receiver requires authentication. Tickory masks saved header values when it reads the config back, so secrets are not shown in plain text after save.

URL Requirements

  • Only http:// and https:// endpoints are accepted.
  • The hostname must resolve publicly. localhost, loopback addresses, and private-network IP ranges are rejected.
  • If you add webhook headers or choose a payload preset, a webhook URL is required.
  • Tickory currently accepts up to 20 custom webhook headers.

Reserved headers are set by Tickory and cannot be overridden from the UI:

  • Content-Type
  • User-Agent
  • Host
  • Idempotency-Key
  • Any header beginning with X-Tickory-

Header names must contain only letters, digits, and hyphens. Header values must be non-empty strings without control characters.

Payload Formats

Tickory sends one of two payload contracts: the legacy Default format or the versioned OpenClaw envelope. Both support individual alerts and aggregated events.

Default Payload: Individual Alert

The legacy individual alert payload uses payload_version: "1.0" and sends symbol-level indicator data.

{
  "payload_version": "1.0",
  "symbol": "BTCUSDT",
  "exchange": "binance",
  "price": 84500.12,
  "rsi_14": 28.3,
  "volume_quote": 1450000000,
  "timestamp": "2026-03-16T11:45:00Z",
  "metrics": {
    "rsi_14": 28.3,
    "close": 84500.12,
    "volume_quote": 1450000000
  },
  "chart_url": "https://www.binance.com/en/futures/BTCUSDT",
  "expression": "has_rsi_14 && rsi_14 < 30",
  "timeframe": "1m"
}
FieldTypeNotes
payload_versionstringAlways 1.0 for legacy single-symbol alerts.
symbolstringMatched market symbol such as BTCUSDT.
exchangestringCurrent exchange identifier, typically binance.
price, rsi_14, volume_quotenumberPrimary market data included for the matched symbol.
timestampstringRFC 3339 timestamp for the matched candle.
metricsobjectMetric map of matched values.
chart_url, expression, timeframestringScan context used to render the alert.

Default Payload: Summary / Batch Events

Tickory batches high-volume runs. When a scan finds 4 or more matches, it emits a single summary event instead of one alert per symbol.

{
  "payload_version": "1.1",
  "event_type": "summary",
  "scan_name": "Momentum Reversal",
  "scan_run_id": "run_01hsw3d6a2n9xq",
  "timestamp": "2026-03-16T11:45:00Z",
  "manage_url": "https://app.tickory.app/dashboard/alerts",
  "total_matches": 6,
  "top_symbols": [
    {
      "symbol": "BTCUSDT",
      "price": 84500.12,
      "volume_quote": 1450000000
    },
    {
      "symbol": "ETHUSDT",
      "price": 4200.8,
      "volume_quote": 980000000
    }
  ]
}
FieldTypeNotes
payload_versionstringAlways 1.1 for aggregated legacy events.
event_typestringsummary or throttle_summary.
scan_name, scan_run_idstringNames the originating scan and run.
timestamp, manage_urlstringDelivery timestamp and manage-alerts link.
total_matches, top_symbolsnumber, arrayPresent on summary events.
suppressed_count, daily_limit, sent_today, reset_atnumber, stringPresent on throttle_summary when a daily limit is hit.

OpenClaw Preset

The OpenClaw preset normalizes every event into the same envelope and adds explicit version fields. Choose this if you want a stable contract for receivers that process both single and summary alerts.

{
  "payload_version": "2.0",
  "contract_version": "tickory.openclaw.trigger.v1",
  "event_id": "evt_01hsw3d6a2n9xq",
  "idempotency_key": "scan:user:BTCUSDT:1773661500",
  "event_type": "alert",
  "scan_id": "scan_01hsw3b2k8jv0m",
  "scan_name": "Momentum Reversal",
  "scan_run_id": "run_01hsw3d6a2n9xq",
  "symbol": "BTCUSDT",
  "matched_at": "2026-03-16T11:45:00Z",
  "sent_at": "2026-03-16T11:45:01Z",
  "evidence_summary": {
    "expression": "has_rsi_14 && rsi_14 < 30",
    "timeframe": "1m",
    "total_matches": 0,
    "top_symbols": [],
    "suppressed_count": 0,
    "daily_limit": 0,
    "sent_today": 0,
    "reset_at": "",
    "metrics": {
      "price": 84500.12,
      "rsi_14": 28.3,
      "volume_quote": 1450000000,
      "matched_metrics": {
        "rsi_14": 28.3,
        "close": 84500.12,
        "volume_quote": 1450000000
      }
    }
  },
  "data": {
    "exchange": "binance",
    "contract_type": "perp",
    "chart_url": "https://www.binance.com/en/futures/BTCUSDT",
    "manage_url": "https://app.tickory.app/dashboard/alerts",
    "timestamp_unix": 1773661500
  }
}

Common OpenClaw fields:

  • contract_version is currently tickory.openclaw.trigger.v1.
  • event_id identifies the delivery event.
  • idempotency_key stays stable across retries.
  • event_type is alert, summary, or throttle_summary.
  • evidence_summary holds expression, timeframe, aggregation details, and matched metrics.
  • data holds exchange, contract type, chart URL, manage URL, and Unix timestamp.

Aggregation fields vary by event type

In the OpenClaw contract, total_matches, top_symbols, and throttle-related values live inside evidence_summary. For plain alert events those fields are usually zero or empty; they become meaningful on summary and throttle_summary.

Signing & Verification

Tickory always sends JSON with a fixed User-Agent and a delivery timestamp. When webhook signing is enabled for your deployment, Tickory also sends HMAC headers you can verify on receipt.

  • Content-Type: application/json
  • User-Agent: Tickory/1.0
  • X-Tickory-Timestamp as a Unix timestamp string
  • X-Tickory-Event-ID when the payload includes event_id
  • Idempotency-Key when the payload includes idempotency_key
  • X-Tickory-Signature-Version: v1 when signing is enabled
  • X-Tickory-Signature: v1=<hex_hmac> when signing is enabled

Tickory signs the exact byte sequence "<unix_timestamp>.<raw_request_body>" using HMAC-SHA256.

Current signing model

The scan UI does not expose a per-alert webhook secret field today. Use custom headers for auth from Tickory to your receiver. Signature verification is deployment-level only today: when the operator setsWEBHOOK_EVENT_SIGNING_KEY, deliveries include X-Tickory-Signature and you verify against that deployment secret.

Node.js

import crypto from "node:crypto";

function verifyTickorySignature({
  rawBody,
  signatureHeader,
  timestampHeader,
  signingSecret,
}: {
  rawBody: Buffer;
  signatureHeader: string;
  timestampHeader: string;
  signingSecret: string;
}) {
  const expected = "v1=" + crypto
    .createHmac("sha256", signingSecret)
    .update(`${timestampHeader}.`)
    .update(rawBody)
    .digest("hex");

  const actual = Buffer.from(signatureHeader, "utf8");
  const wanted = Buffer.from(expected, "utf8");

  if (actual.length !== wanted.length) {
    return false;
  }

  return crypto.timingSafeEqual(actual, wanted);
}

Python

import hashlib
import hmac


def verify_tickory_signature(raw_body: bytes, signature_header: str, timestamp_header: str, signing_secret: str) -> bool:
    signed_payload = f"{timestamp_header}.".encode("utf-8") + raw_body
    digest = hmac.new(
        signing_secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()
    expected = f"v1={digest}"
    return hmac.compare_digest(signature_header, expected)

Go

package main

import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "fmt"
)

func verifyTickorySignature(rawBody []byte, signatureHeader, timestampHeader, signingSecret string) bool {
  mac := hmac.New(sha256.New, []byte(signingSecret))
  mac.Write([]byte(timestampHeader))
  mac.Write([]byte("."))
  mac.Write(rawBody)

  expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
  return hmac.Equal([]byte(signatureHeader), []byte(expected))
}

func main() {
  fmt.Println("verify in your HTTP handler")
}

In production, also reject requests with stale X-Tickory-Timestamp values to protect against replay attacks.

Retry Behavior

  • Success: any 2xx response marks the delivery successful.
  • Immediate HTTP retries: network failures and 5xx responses are retried up to 3 times inside one worker attempt with short backoff.
  • Queued delivery retries: alert events are currently queued with 3 total worker attempts. If a worker attempt fails, Tickory schedules the next retry about 1 minute later, then about 5 minutes later before the final attempt.
  • 4xx responses: these stop the immediate in-attempt retry loop, but the delivery still fails and may be retried later by the queue worker.
  • Timeouts: expect roughly a 10-second timeout per outbound HTTP request attempt.

Because retries preserve Idempotency-Key and X-Tickory-Event-ID, receivers should deduplicate writes on one of those keys.

Testing

Use the built-in test alert after saving your webhook config. Test deliveries send an alertevent with sample market data and a synthetic symbol such as TEST-BTC.

For local development, expose your receiver with a tunnel because localhost and private-network targets are blocked by production SSRF protection.

ngrok http 3000
  1. Run your local receiver on port 3000.
  2. Start ngrok and copy the public HTTPS URL.
  3. Paste that URL into the Tickory webhook destination.
  4. Save the config and send a test alert.
  5. Inspect headers, raw body bytes, and signature verification in your local logs.

Recommended receiver behavior

Store the raw request body before JSON parsing, verify the signature if present, acknowledge quickly with a 2xx, and process the payload asynchronously behind your own queue if downstream work is expensive.

Need broader alerting context first? See Alerts & Notifications.