theuxbites webhooks
theuxbites can send a signed JSON event to an address you choose when something happens in your account: an audit finishes, the daily watch sees something worse, a site audit or a prospect list is done, or an audit speaks to an issue you marked fixed. The address can be your own server, a Zapier “Catch Hook” or a Make “Custom webhook”, so the events can reach Slack, a spreadsheet, a ticket or a CRM without code.
Part of the Studio plan. See plans.
Set it up
- In theuxbites, open the user menu and choose Webhooks.
- Paste the address that should receive the events. It must start with
https://, use the standard port and resolve to a public internet address. Up to 5 addresses per account. - Tick the events it should receive.
- Choose “Send test event”. A
webhook.testevent is sent at once, signed like any other, and the page says what your endpoint answered. - On your own server, check the signature (below) with the secret shown under “Show secret”.
Each endpoint has a switch to stop it, a delivery log of its last 30 events with what was sent and what came back, and “Resend” for any of them.
What is sent
A POST with a JSON body. Every event has the same envelope: id (also in the webhook-id header), type, created_at and data.
{
"id": "evt_3f9c2b7a51d84e0c9a6b1d2e4f5a6b7c",
"type": "audit.completed",
"created_at": "2026-10-24T09:12:31.000Z",
"data": {
"report_id": "6b1f…",
"project_id": "0c42…",
"url": "https://example.com/pricing",
"completed_at": "2026-10-24T09:12:30.512Z",
"ux_score": 78,
"scores": { "performance": 64, "accessibility": 92, "seo": 100, "ai_visibility": 85 },
"findings": { "total": 14, "critical": 1, "quick_wins": 6, "planned": 7 },
"report_url": "https://theuxbites.com/projects/0c42…"
}
}The events say what an audit or a check observed, in the same words as the report and the emails. No AI writes any of it. Scores are 0 to 100; a part the run could not measure is null, not 0. Fields may be added over time; a receiver should ignore what it does not know.
Events
Audit completed
audit.completedAn audit of a page finished, whoever started it: you, a schedule, your assistant, a bulk list, a site audit or a competitor comparison.
report_id- The audit.
project_id- The project the page belongs to.
url- The audited page.
completed_at- When the audit finished (ISO 8601).
ux_score- The UX score, 0 to 100, the same number the report shows.
scores- performance, accessibility, seo and ai_visibility, 0 to 100, or null when the run could not measure that part.
findings- total, critical, quick_wins and planned: the counts the report shows.
report_url- The project in theuxbites (sign-in needed).
Audit did not complete
audit.failedAn audit stopped without a result. It is not counted against your monthly allowance.
report_id- The audit.
project_id- The project, or null when the audit was not linked to one yet.
url- The page.
failed_at- When it was recorded (ISO 8601).
Daily watch found something worse
watch.alertThe daily watch of a project saw at least one change for the worse: a page that stopped loading, got noindex, lost its title, or a robots.txt that started blocking AI search crawlers. Sent once per check, like the email.
project_id- The watched project.
checked_at- When the check ran (ISO 8601).
changes- Every change of this check, worse first: url, kind, direction ("worse" or "better"), title and detail, the same sentences as the email.
project_url- The project in theuxbites.
Site audit finished
site_audit.completedEvery page of a site audit has finished, one way or the other.
site_audit_id- The site audit.
project_id- Its project.
started_at- When it was started (ISO 8601).
pages- total, completed, failed and skipped (not audited because the allowance ran out).
page_list- Each page: url, status, report_id and reused (an audit from the last day was used at no cost).
site_audit_url- The Site audit tab in theuxbites.
Prospect list finished
prospect_batch.completedEvery address of a bulk prospect list has finished, one way or the other.
batch_id- The list.
created_at- When it was started (ISO 8601).
pages- total, completed, failed and skipped.
items- Each address: url, status, project_id and report_id.
list_url- The list in theuxbites, with the extracts.
An audit spoke to an issue marked fixed
fix.checkedThe first audit after an issue was marked fixed that measured that part of the page, and any later audit whose answer differs from the one before (found again, or no longer found).
project_id- The project.
page_url- The page the issue was marked on.
issue_id- The finding, as list_issues names it over MCP.
title- The finding’s title.
marked_fixed_at- When it was marked fixed (ISO 8601).
audit_at- When the audit ran (ISO 8601).
report_id- The audit.
observation- "not_found": the audit measured this part of the page and did not report the issue. "found": it reported it.
note- A sentence saying that one audit is an observation, not proof of a fix.
fix_log_url- The Fix log tab in theuxbites.
About fix.checked: One audit is an observation of the page on that day, not proof that a fix worked or did not. It follows the same rule as the Fix log tab, so the two never disagree.
Check the signature
Events are signed the Standard Webhooks way, so its libraries (for example standardwebhooks on npm and PyPI) and Svix’s can check them. Three headers come with every attempt:
webhook-id: the event’s id, the same on every retry, so a repeat can be dropped.webhook-timestamp: seconds since 1970 when this attempt was sent.webhook-signature:v1,followed by the base64 HMAC-SHA256 ofid.timestamp.body, keyed with the base64-decoded part of the secret afterwhsec_. There may be more than one, separated by spaces; one match is enough.
import { createHmac, timingSafeEqual } from 'node:crypto';
// secret: "whsec_…" from the Webhooks page. body: the raw request body, unparsed.
export function verify(secret, headers, body) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'] ?? '';
// Refuse old or future events (replays): five minutes either way.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64');
return signatures.split(' ').some(entry => {
const given = entry.split(',')[1] ?? '';
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
});
}Sign the body exactly as it arrived: parsing and re-serialising the JSON changes the bytes. “Roll secret” makes a new secret, and the old one stops working at once.
Answering, retries and switching off
Answer with any 2xx status as soon as the event is stored; do slow work afterwards. A redirect is not followed and counts as not received, so give the final address. If your endpoint does not answer with a 2xx status within 10 seconds, the event is sent again after about 30 seconds and 2 minutes, then once a day for three days. Every attempt carries the same webhook-id, so a repeat can be recognised and dropped.
After 5 events in a row that could not be delivered after every retry, the endpoint is switched off and the settings page says why. Switch it on again when it is fixed.
The order of events is not guaranteed; use created_at and the ids in data. If the account leaves the Studio plan, the endpoints are kept and nothing is sent until the plan includes webhooks again.
Your data
theuxbites keeps each endpoint’s address, name and events, and a log of what it sent there: the event, the status your server answered and when. Signing secrets are not stored; they are derived on the server when an event is signed. What your endpoint does with an event is up to you and the service it belongs to. See the privacy policy.
Questions: hello@theuxbites.com