Webhooks
Genvoris pushes lifecycle and quota events to your backend in real time. Configure endpoints in Dashboard → Webhooks. Each endpoint has its own signing secret (shown once at creation).
:::danger Signature verification is REQUIRED
You MUST verify the X-Genvoris-Signature header before processing any webhook payload. Skipping verification lets an attacker forge events (fake quota grants, fake cancellations, fake customer creates) by POSTing to your public webhook URL — this is a critical security vulnerability.
The verification snippets below use timingSafeEqual / hash_equals for constant-time comparison and a 300-second timestamp window for replay protection. Do not skip either step.
:::
Events
| Event | Fires when |
|---|---|
tryon.completed | A try-on generation succeeds and usage/credit attribution is recorded. |
tryon.failed | A try-on request fails after validation or model execution. |
customer.plan_changed | A customer's assigned plan changes. |
customer.quota_exhausted | A try-on was rejected for quota — fired even on the rejecting request. |
credit.low_balance | Store credit balance crosses the configured low-balance threshold. |
credit.balance_added | Credits are purchased, granted, or otherwise added to the store balance. |
Legacy aliases
Older integrations may still receive these aliases; new integrations should prefer the event names above.
| Event | Fires when |
|---|---|
end_customer.created | First time a customer is upserted via POST /v1/customers. |
end_customer.updated | A customer is upserted again or PATCH'd. |
end_customer.cancelled | A customer is DELETE'd (soft cancel). |
end_customer.quota_warning | Customer crosses 80% of plan quota in current period (once per period). |
end_customer.quota_exhausted | Legacy alias for customer.quota_exhausted. |
end_customer.period_rolled | Period auto-rolled to a new 30-day window. |
plan.created | A plan was created. |
plan.updated | A plan was updated. |
plan.disabled | A plan was soft-deleted. |
Payload shape
Every delivery is a JSON envelope:
{
"id": "evt_8f3a...",
"type": "end_customer.quota_warning",
"created": 1745812345,
"data": {
"customer_id": "ec_abc",
"external_id": "user_42",
"plan_id": "pln_xxxx",
"used": 80,
"limit": 100,
"remaining": 20,
"threshold": 0.8
}
}
Signature header
X-Genvoris-Signature: t=1745812345,v1=4ef9...c2
X-Genvoris-Event: end_customer.quota_warning
X-Genvoris-Delivery: whd_abcd1234
The signed string is ${t}.${rawRequestBody}, HMAC-SHA256 with your endpoint secret.
Verifying — Node
With the SDK (recommended)
import { WebhooksResource } from '@genvoris/node';
const event = WebhooksResource.verify({
payload: req.body, // Buffer of the raw request body
header: req.header('x-genvoris-signature') ?? '',
secret: process.env.GENVORIS_WEBHOOK_SECRET!,
toleranceSeconds: 300, // optional, default 300
});
// event.type, event.id, event.created, event.data
The SDK handles parsing, timestamp validation, constant-time comparison, and byte-length guarding — see the Node SDK reference.
Manual implementation
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(
header.split(',').map(p => p.split('=').map(s => s.trim()))
);
const ts = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
Verifying — PHP
With the Laravel package (recommended)
The genvoris/laravel package auto-verifies incoming webhooks via the VerifyGenvorisWebhook middleware — see the Laravel integration docs. No manual code needed.
Manual implementation
function gv_verify($secret, $rawBody, $header) {
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $m);
if (!$m) return false;
if (abs(time() - (int)$m[1]) > 300) return false;
$expected = hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret);
return hash_equals($expected, $m[2]);
}
Delivery semantics
- Timeout per attempt: 10 s.
- Success: any
2xx. - Retry on: anything else, plus network errors.
- Backoff (seconds):
10, 30, 120, 300, 900, 1800, 2700, 3600. - Max attempts: 8. After that the delivery is marked
DEADand not retried again. - Ordering: not guaranteed. Use the envelope
idfor idempotency on your side.
Test ping
Each endpoint has a "Send test ping" button in the dashboard. It dispatches a synthetic webhook.test event with body {"message": "Hello from Genvoris"} so you can verify your handler before going live.
Disabling
Disabling an endpoint stops new dispatches. Pending retries for already-failed deliveries are also skipped.