Skip to main content
Webhooks let you receive results via POST when a run finishes, instead of polling. Two ways to set them up:
MethodScopeUse case
SubscriptionsAll runs in your orgPipelines, logging, CRM sync
Per-run callbackUrlSingle runOne-off jobs, per-request routing
You can use both at the same time. If a run matches a subscription and also has a callbackUrl, both endpoints get a delivery.
For async basics (fire-and-forget, polling with client.wait()), see Runs.

Webhook Subscriptions

Subscribe to events across all runs in your org. You can manage subscriptions through the API or the dashboard.

Create a Subscription

curl -X POST https://api.subconscious.dev/v1/webhooks/subscriptions \
  -H "Authorization: Bearer $SUBCONSCIOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-server.com/webhooks/subconscious",
    "eventTypes": ["job.succeeded", "job.failed"],
    "secret": "your-signing-secret",
    "description": "Production webhook"
  }'
The response includes the plaintext secret. This is the only time you’ll see it, so store it for HMAC verification.

Event Types

EventFires when
job.succeededRun completed successfully
job.failedRun failed or timed out

Enable / Disable

Subscriptions have an enabled toggle. Disabling a subscription skips delivery but keeps the config and history around. Events that fire while disabled aren’t retroactively sent when you re-enable. Toggle in the dashboard or through the API:
# List subscriptions (secrets are masked)
GET /v1/webhooks/subscriptions

# Delete a subscription
DELETE /v1/webhooks/subscriptions/:id

Signing & Verification

If you set a secret, every delivery includes HMAC-SHA256 headers:
HeaderValue
X-Webhook-SignatureHMAC-SHA256 hex digest of the payload
X-Webhook-TimestampUnix timestamp of the delivery
X-Webhook-Delivery-IdUnique delivery identifier
Secrets are encrypted at rest with AES-256-GCM and only decrypted at signing time.
import hashlib, hmac

def verify_webhook(payload_bytes: bytes, secret: str, signature: str) -> bool:
    expected = hmac.new(secret.encode(), payload_bytes, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Per-Run Callbacks

For one-off deliveries, pass callbackUrl on the run request:
curl -X POST https://api.subconscious.dev/v1/runs \
  -H "Authorization: Bearer $SUBCONSCIOUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "engine": "tim-gpt",
    "input": { "instructions": "Generate a report" },
    "output": { "callbackUrl": "https://your-server.com/webhooks/subconscious" }
  }'
Returns immediately with a runId. We POST the result to your URL when the run finishes.

Webhook Payload

Subscriptions and per-run callbacks use the same payload format:
{
  "jobId": "acf121b1-2ec8-498a-8d65-68be0f5adec7",
  "runId": "bcccd0a3-4153-4844-b806-b651ccab94cc",
  "orgId": "b4dec88d-9392-486a-b8e4-f39fb3ba2472",
  "status": "succeeded",
  "engine": "tim-gpt",
  "result": {
    "fullResponse": "{\"reasoning\": [...], \"answer\": \"Hello! How can I help you today?\"}"
  },
  "error": null,
  "tokens": {
    "inputTokens": 1980,
    "outputTokens": 406,
    "costCents": 0
  },
  "createdAt": "2026-03-26T20:06:37.956Z",
  "startedAt": null,
  "completedAt": "2026-03-26T20:06:46.777Z",
  "metadata": null
}
FieldDescription
runIdThe run’s unique ID
statussucceeded, failed, timed_out, or canceled
resultContains fullResponse, a JSON string with reasoning steps and answer
errorError details when failed, otherwise null
tokensToken counts (inputTokens, outputTokens) and costCents
startedAtWhen processing started, null if it hasn’t yet
metadataPassed through from input.metadata if you set it

Webhook Handler

import json
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/subconscious")
async def handle_webhook(request: Request):
    payload = await request.json()

    if payload["status"] == "succeeded":
        full = json.loads(payload["result"]["fullResponse"])
        print(f"Run {payload['runId']}: {full['answer']}")
    elif payload["status"] == "failed":
        print(f"Run {payload['runId']} failed: {payload.get('error')}")

    return {"received": True}
Your endpoint needs to be publicly accessible. For local dev, use ngrok or similar.

Delivery Guarantees

  • SQS-backed: all deliveries go through a durable queue with retries and exponential backoff
  • Timeout: we wait up to 30 seconds for your 2xx response
  • Dead-letter queue: exhausted retries are stored for inspection
  • Delivery log: check delivery history and payloads in the dashboard

Dashboard

Manage webhooks at subconscious.dev/platform/webhooks:
  • Create, enable/disable, and delete subscriptions
  • Send test webhooks
  • View delivery log with status, timestamps, and expandable payloads
  • Reveal signing secret

Runs

Async patterns and polling

Error Handling

Handle failed runs