Skip to main content

Webhooks

Steward delivers real-time event notifications to your configured webhook endpoints. Every transaction state change, policy violation, and spend threshold crossing fires a signed HTTP POST to your URL. Base path: /webhooks
Auth: Tenant owner/admin session with recent MFA verification

Event Types

EventFired when
tx.pendingA transaction is queued for human approval
tx.approvedA pending transaction is approved
tx.deniedA pending transaction is denied
tx.signedA transaction is signed (and optionally broadcast)
spend.thresholdAn agent’s spend tracking threshold is crossed
policy.violationA hard policy rejects a transaction
You can subscribe to any subset of events or omit events to receive all of them.

Register a Webhook

POST /webhooks
curl -X POST https://api.steward.fi/webhooks \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/steward",
    "events": ["tx.pending", "tx.signed", "policy.violation"],
    "description": "Main webhook for agent notifications",
    "maxRetries": 5,
    "retryBackoffMs": 60000
  }'
Response:
{
  "ok": true,
  "data": {
    "id": "wh_550e8400-...",
    "tenantId": "your-tenant",
    "url": "https://your-app.com/webhooks/steward",
    "events": ["tx.pending", "tx.signed", "policy.violation"],
    "secret": "whsec_a1b2c3d4...",
    "enabled": true,
    "maxRetries": 5,
    "retryBackoffMs": 60000,
    "description": "Main webhook for agent notifications",
    "createdAt": "2026-03-27T00:00:00Z"
  }
}
The secret is only returned on creation. Store it securely — you’ll need it to verify incoming webhook signatures.
Body fields:
FieldRequiredDescription
urlHTTPS endpoint to receive events
eventsArray of event types to subscribe to (default: all)
descriptionHuman-readable label
maxRetriesMax delivery attempts on failure (0–10, default: 5)
retryBackoffMsMilliseconds between retries (min: 1000, default: 60000)

List Webhooks

GET /webhooks
curl https://api.steward.fi/webhooks \
  -H "X-Steward-Key: your-tenant-key"
Returns all webhooks for the tenant. The secret field is omitted from list responses.

Update a Webhook

PUT /webhooks/:id
curl -X PUT https://api.steward.fi/webhooks/wh_550e8400-... \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false,
    "events": ["tx.pending", "tx.approved", "tx.denied"]
  }'
Updatable fields: url, events, enabled, description, maxRetries, retryBackoffMs.

Delete a Webhook

DELETE /webhooks/:id
curl -X DELETE https://api.steward.fi/webhooks/wh_550e8400-... \
  -H "X-Steward-Key: your-tenant-key"

Delivery History

GET /webhooks/:id/deliveries?limit=50&offset=0&status=failed&eventType=tx.pending&hasError=true
Returns recent delivery attempts with redacted status metadata. Payloads, webhook URLs, secrets, and raw error text are intentionally omitted from this list response. Optional filters include status, eventType, and hasError.
{
  "ok": true,
  "data": [
    {
      "id": "del_...",
      "eventType": "tx.signed",
      "status": "delivered",
      "attempts": 1,
      "maxAttempts": 6,
      "nextRetryAt": null,
      "hasError": false,
      "createdAt": "2026-03-27T12:34:56Z",
      "deliveredAt": "2026-03-27T12:34:57Z"
    },
    {
      "id": "del_...",
      "eventType": "tx.pending",
      "replayedFromDeliveryId": "del_original_...",
      "status": "failed",
      "attempts": 5,
      "maxAttempts": 6,
      "nextRetryAt": "2026-03-27T12:35:56Z",
      "hasError": true,
      "createdAt": "2026-03-27T12:34:56Z",
      "deliveredAt": null
    }
  ]
}

Export Delivery History

GET /webhooks/:id/deliveries/export?limit=1000&status=failed&eventType=tx.pending&hasError=true
Downloads the same redacted delivery-history fields as CSV for offline incident review. The export is bounded to 10,000 rows, supports the same filters as the JSON history endpoint, omits payloads, webhook URLs, secrets, and raw error text, and escapes spreadsheet formula prefixes in every cell.

Retry a Failed Delivery

POST /webhooks/deliveries/:id/retry
Queues an eligible failed delivery for immediate retry without resetting its attempt count or bypassing the configured retry budget.

Replay a Historical Delivery

POST /webhooks/deliveries/:id/replay
Creates and sends a new signed delivery from a historical delivery payload. Replay is separate from retry: retry reschedules the original failed delivery and preserves its attempt budget, while replay creates a new delivery ID and signature so receivers can distinguish it from the original. Replay requires an owner/admin session with recent MFA, and Steward only replays if the original webhook still exists, is enabled, has the same URL, and is still subscribed to the event type. Pending or processing deliveries cannot be replayed. The response is the new redacted delivery. Replayed deliveries include replayedFromDeliveryId.

Send a Diagnostic Test Delivery

POST /webhooks/:id/test
Sends a one-off signed webhook.test delivery to an enabled endpoint. Test deliveries are diagnostic only; webhook.test is not a subscribable event type and is not added to endpoint event filters.

Payload Format

All events share the same envelope:
{
  "id": "evt_abc123",
  "event": "tx.signed",
  "tenantId": "your-tenant",
  "agentId": "agent-1",
  "timestamp": "2026-03-27T12:34:56.789Z",
  "data": { ... }
}
Event-specific data fields:
{
  "approvalId": "appr_...",
  "txId": "tx_...",
  "to": "0xSomeAddress",
  "value": "500000000000000000",
  "chainId": 8453,
  "policyResults": [
    { "policyId": "auto-threshold", "type": "auto-approve-threshold", "passed": false, "reason": "Value exceeds threshold" }
  ]
}
{
  "txId": "tx_...",
  "txHash": "0xabc...",
  "to": "0xSomeAddress",
  "value": "10000000000000000",
  "chainId": 8453,
  "broadcast": true
}
{
  "txId": "tx_...",
  "to": "0xBlockedAddress",
  "value": "1000000000000000000",
  "failedPolicies": [
    { "policyId": "whitelist", "type": "approved-addresses", "passed": false, "reason": "Address not in whitelist" }
  ]
}

Verifying Signatures

Every delivery includes an X-Steward-Signature header. Verify it to ensure the payload came from Steward:
import { createHmac } from "crypto";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return `sha256=${expected}` === signature;
}

// Express example
app.post("/webhooks/steward", (req, res) => {
  const sig = req.headers["x-steward-signature"] as string;
  const body = JSON.stringify(req.body);

  if (!verifyWebhookSignature(body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  const { event, agentId, data } = req.body;

  if (event === "tx.pending") {
    // Notify your team to review the pending transaction
    await notifyAdmin(agentId, data.approvalId, data.value);
  }

  res.json({ ok: true });
});

Delivery Guarantees

  • Steward delivers with at-least-once semantics — design your handler to be idempotent
  • Failed deliveries are retried up to maxRetries times with retryBackoffMs spacing
  • Your endpoint should respond 2xx within 10 seconds or the attempt is counted as failed
  • Delivery history is kept for 30 days

  • Approvals — Review and act on tx.pending events
  • Tenant Config — Enable webhookCallbackEnabled in approval config