Webhooks
Hubrix processes inbound webhooks from Stripe for billing events. Outbound webhooks (Hubrix → your server) are in preview and not yet active in production.
Inbound webhooks (Stripe)
Stripe sends billing events to Hubrix at POST https://api.hubrix.ai/v1/billing/webhook. This endpoint is public (no Bearer token) but requires a valid Stripe HMAC signature.
Handled events
| Stripe event | Hubrix action |
|---|---|
checkout.session.completed | Create/update subscription, allocate monthly credits, send subscription_started email |
invoice.payment_succeeded | Renew subscription period, re-allocate credits, send subscription_renewed email |
invoice.payment_failed | Set subscription to past_due, send payment_failed email |
customer.subscription.updated | Update plan, seats, cancel_at_period_end flag |
customer.subscription.deleted | Set subscription to cancelled, send subscription_cancelled email |
customer.subscription.trial_will_end | Send trial_ending_in_2d email (Stripe fires 3 days before) |
payment_intent.succeeded | Record payment in stripe_payments for top-up idempotency |
Stripe webhook payload shapes are documented in the Stripe API reference. We do not duplicate them here.
Outbound webhooks (preview)
Preview. Outbound webhooks are not yet active. This documents the planned behaviour; subscribe at api@hubrix.ai for early access.
When outbound webhooks are live, configure endpoints from Settings → Webhooks in the Hubrix UI. You can subscribe per-event or to all events.
Planned events
| Event type | Trigger | Key payload fields |
|---|---|---|
workflow.run.completed | Workflow run reaches completed or failed status | run_id, workflow_id, status, outputs, error |
research.report.ready | Research report reaches completed status | report_id, query, depth, content_length |
bulk.job.completed | Bulk job finishes (all rows processed) | job_id, total_rows, rows_completed, rows_failed |
connector.sync.failed | Connector sync job fails after retries | connector_id, folder_id, error, attempt |
Payload structure
{
"id": "evt_01HZ3K9ABCDEF",
"type": "workflow.run.completed",
"created_at": "2026-05-04T17:00:00Z",
"company_id": "uuid-of-company",
"data": {
"run_id": "uuid-of-run",
"workflow_id": "uuid-of-workflow",
"status": "completed",
"outputs": { "result": "..." },
"error": null
}
}Signature verification
Every outbound request includes X-Hubrix-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>. Compute the expected signature as:
HMAC-SHA256(secret, "{timestamp}.{request_body}")Verification examples:
# Python
import hmac, hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature.split(","))
ts, sig = parts.get("t", ""), parts.get("v1", "")
expected = hmac.new(
secret.encode(),
f"{ts}.{body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig)// JavaScript
import crypto from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const parts = Object.fromEntries(signature.split(',').map(p => p.split('=')));
const { t: ts, v1: sig } = parts;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${body}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Retry policy
If your endpoint returns a non-2xx status, Hubrix retries with this schedule:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | +30 seconds |
| 3 | +5 minutes |
| 4 | +1 hour |
| 5 | +6 hours |
| DLQ | After 5 failures, event moves to dead-letter queue |
Each delivery includes an Idempotency-Key header (UUID) so you can safely de-duplicate retried events.
Receiving webhook events
Your endpoint must:
- Accept
POSTrequests withContent-Type: application/json. - Respond with
2xxwithin 30 seconds. - Verify the
X-Hubrix-Signatureheader before processing. - Return
200even for events you don't handle (prevents unnecessary retries).