Skip to main content

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

EventFires when
tryon.completedA try-on generation succeeds and usage/credit attribution is recorded.
tryon.failedA try-on request fails after validation or model execution.
customer.plan_changedA customer's assigned plan changes.
customer.quota_exhaustedA try-on was rejected for quota — fired even on the rejecting request.
credit.low_balanceStore credit balance crosses the configured low-balance threshold.
credit.balance_addedCredits 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.

EventFires when
end_customer.createdFirst time a customer is upserted via POST /v1/customers.
end_customer.updatedA customer is upserted again or PATCH'd.
end_customer.cancelledA customer is DELETE'd (soft cancel).
end_customer.quota_warningCustomer crosses 80% of plan quota in current period (once per period).
end_customer.quota_exhaustedLegacy alias for customer.quota_exhausted.
end_customer.period_rolledPeriod auto-rolled to a new 30-day window.
plan.createdA plan was created.
plan.updatedA plan was updated.
plan.disabledA 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

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

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 DEAD and not retried again.
  • Ordering: not guaranteed. Use the envelope id for 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.