Skip to content
JAOT

Webhooks

Webhooks deliver real-time notifications to your server when events occur in JAOT. Instead of polling for execution results, register a webhook URL and receive HTTP POST callbacks when solves complete or fail.

Overview

JAOT supports webhooks through the trigger system. When you create a trigger with a webhook_url, solve results are automatically delivered to that URL when the run completes.

Webhook flow:

  1. Create a trigger with a webhook_url and webhook_secret
  2. Fire the trigger (manually or via automation)
  3. When the solve completes, JAOT POSTs the result to your webhook URL
  4. Your server verifies the signature and processes the result

Warning: Always verify webhook signatures to prevent spoofed requests. Use the webhook_secret you provided during trigger creation to validate the HMAC-SHA256 signature.


Register a webhook

Webhooks are configured per-trigger. Set webhook_url and webhook_secret when creating or updating a trigger.

POST /api/v2/triggers
import requests

response = requests.post(
    "https://api.jaot.io/api/v2/triggers",
    headers={"Authorization": "Bearer ok_live_..."},
    json={
        "name": "Inventory optimizer with webhook",
        "document_id": "doc_abc123",
        "version_id": "ver_def456",
        "webhook_url": "https://your-app.com/webhooks/jaot",
        "webhook_secret": "whsec_your_signing_secret_here"
    }
)

trigger = response.json()
print(f"Trigger created: {trigger['id']}")
print(f"Webhook URL: {trigger['webhook_url']}")

Update a webhook URL

Update the webhook configuration on an existing trigger.

PATCH /api/v2/triggers/{trigger_id}
response = requests.patch(
    "https://api.jaot.io/api/v2/triggers/trg_a1b2c3d4",
    headers={"Authorization": "Bearer ok_live_...", "Content-Type": "application/json"},
    json={
        "webhook_url": "https://new-endpoint.com/webhooks/jaot",
        "webhook_secret": "whsec_new_signing_secret",
    },
)

Webhook payload

When a trigger run completes, JAOT sends an HTTP POST to your webhook_url with the solve result.

Payload structure

{
  "event": "trigger.run.completed",
  "trigger_id": "trg_a1b2c3d4",
  "run_id": "run_x1y2z3w4",
  "timestamp": "2026-02-19T06:00:15Z",
  "data": {
    "status": "completed",
    "result": {
      "status": "optimal",
      "objective_value": 4250.0,
      "solution": {
        "warehouse_a_units": 120,
        "warehouse_b_units": 85,
        "warehouse_c_units": 200
      },
      "solve_time_seconds": 2.3,
      "credits_used": 3,
      "credits_remaining": 847
    },
    "override_data": {
      "demand_forecast": [120, 85, 200, 150, 90]
    }
  }
}

Payload fields

FieldTypeDescription
eventstringEvent type (e.g., trigger.run.completed, trigger.run.failed)
trigger_idstringTrigger that was fired
run_idstringUnique run identifier
timestampstringISO 8601 event timestamp
data.statusstringRun status: completed or failed
data.resultobjectSolve result (same structure as POST /solve response)
data.override_dataobjectOverride values used for this run

Failed run payload

{
  "event": "trigger.run.failed",
  "trigger_id": "trg_a1b2c3d4",
  "run_id": "run_x1y2z3w4",
  "timestamp": "2026-02-19T06:00:15Z",
  "data": {
    "status": "failed",
    "error": "Solver error: problem is infeasible with the given constraints",
    "override_data": {
      "demand_forecast": [120, 85, 200, 150, 90]
    }
  }
}

Signature verification

Every webhook request includes an X-JAOT-Signature header containing an HMAC-SHA256 signature of the request body, signed with your webhook_secret.

Verification algorithm

  1. Read the raw request body (before JSON parsing)
  2. Compute HMAC-SHA256 using your webhook_secret as the key
  3. Compare the computed signature with the X-JAOT-Signature header
  4. Use constant-time comparison to prevent timing attacks
import hashlib
import hmac
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret_here"

@app.route("/webhooks/jaot", methods=["POST"])
def handle_webhook():
    # 1. Get the signature from the header
    signature = request.headers.get("X-JAOT-Signature")
    if not signature:
        abort(401, "Missing signature")

    # 2. Compute expected signature
    raw_body = request.get_data()
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # 3. Constant-time comparison
    if not hmac.compare_digest(signature, expected):
        abort(401, "Invalid signature")

    # 4. Process the event
    payload = request.get_json()
    event = payload["event"]
    run_id = payload["run_id"]

    if event == "trigger.run.completed":
        result = payload["data"]["result"]
        print(f"Run {run_id} completed: objective={result['objective_value']}")
        # Update your database, send notifications, etc.

    elif event == "trigger.run.failed":
        error = payload["data"]["error"]
        print(f"Run {run_id} failed: {error}")
        # Alert your team

    return {"received": True}, 200

Delivery and retries

BehaviorDetails
Timeout30 seconds per delivery attempt
SuccessAny 2xx status code is treated as successful delivery
Content-Typeapplication/json

Tip: Respond to webhook deliveries quickly (within a few seconds). If your processing is slow, acknowledge receipt with a 200 response and process the payload asynchronously in a background job.


Removing a webhook

To stop receiving webhooks, update the trigger to clear the webhook URL or delete the trigger entirely.

Clear webhook URL

response = requests.patch(
    "https://api.jaot.io/api/v2/triggers/trg_a1b2c3d4",
    headers={"Authorization": "Bearer ok_live_...", "Content-Type": "application/json"},
    json={"webhook_url": None},
)

Delete the trigger

response = requests.delete(
    "https://api.jaot.io/api/v2/triggers/trg_a1b2c3d4",
    headers={"Authorization": "Bearer ok_live_..."},
)