What’s Inside Each Webhook Message
Introduction
Every AAArdvark webhook message follows the same structure: a common envelope that tells you which event fired, when, and on which site – and a data section that holds the details specific to that event.
This guide breaks down each part. Once you’ve read the envelope once, all the other event types will feel familiar.
The common wrapper
Here’s the envelope every message arrives in:
{
"version": "1.0",
"event": "issue.created",
"timestamp": "2026-06-04T21:32:50.209952Z",
"site": {
"id": 1428,
"name": "site care",
"url": null,
"base_url": "https://sitecare.com",
"workspace": {
"id": 1,
"name": "Abdulkhalek's workspace"
}
},
"data": {
"...": "event-specific payload — see the per-event sections below"
}
}
| Field | Type | What does it tell you |
|---|---|---|
version | string | The version of this message format. Right now it’s always "1.0". If we ever change the shape in a way that could break things, we’ll bump this number and document the change. |
event | string | Which event happened, e.g. issue.created. |
timestamp | ISO-8601 string | The moment we queued the message (UTC, down to fractions of a second). |
site.id | integer | The numeric ID of the site in AAArdvark. |
site.name | string | The site’s friendly name. |
site.url | string | null | The site URL is set in your dashboard. |
site.base_url | string | The starting point we crawl from for this site. |
site.workspace.id | integer | The numeric ID of the workspace the site lives in. |
site.workspace.name | string | The workspace’s friendly name. |
data | object | The event-specific details. What’s inside depends on which event fired – see the sections below. |
is_replay | boolean | Only appears when the message is part of a “Sync all existing data” backfill. When present, it’s always true; on normal real-time messages, it isn’t sent at all. |
When a new issue is found
This message (issue.created) arrives the first time a particular kind of accessibility problem is logged for your site.
{
"version": "1.0",
"event": "issue.created",
"timestamp": "2026-06-04T21:32:50.209952Z",
"site": {
"id": 1428,
"name": "site care",
"url": null,
"base_url": "https://sitecare.com",
"workspace": { "id": 1, "name": "Abdulkhalek's workspace" }
},
"data": {
"type": "created",
"issue": {
"id": 2745,
"unique_site_issue_id": 1,
"site_id": 1428,
"first_scan_id": 1271,
"scan_id": 1512,
"status": "active",
"issue_type": "automatic",
"failure": "color-contrast",
"principle": "WCAG2:perceivable",
"success_criteria": "1.4.3",
"severity": "Very High",
"priority": "5",
"message": "Elements must meet minimum color contrast ratio thresholds",
"recommended_techniques": ["G18", "G145", "G148", "G174"],
"assignee_name": "Unassigned",
"user_id": null,
"assignee": null,
"reporter_id": null,
"custom_severity": null,
"original_severity": null,
"original_failure": null,
"custom_issue_id": null,
"level": "",
"converted": 0,
"previous_status": null,
"created_at": "2025-08-07T22:46:15.000000Z",
"updated_at": "2025-09-26T01:44:59.000000Z",
"link": "https://staging.aaardvarkaccessibility.com/dashboard/site/1732da/issues/2745",
"payload": { "...": "axe/htmlcs/ace runner output" }
}
}
}
Inside data.issue, the fields worth knowing are:
| Field | Type | What does it tell you |
|---|---|---|
id | integer | Our internal ID for the issue – unique across the whole system. |
unique_site_issue_id | integer | The friendly per-site number you see in the dashboard (#1, #2, #3…). |
site_id | integer | Which site this belongs to (matches site.id in the envelope). |
first_scan_id | integer | The scan that first spotted this issue. |
scan_id | integer | The most recent scan that included it. |
status | string | Where the issue stands: active, resolved_pending, resolved, resolved_manual, or ignored. |
issue_type | string | automatic (found by a scan) or manual (logged by a person). |
failure | string | The kind of WCAG failure, e.g. color-contrast or image-alt. |
principle | string | The WCAG principle it rolls up to, e.g. WCAG2:perceivable. |
success_criteria | string | The specific WCAG success criterion, e.g. 1.4.3. |
severity | string | How serious it is: Critical, Very High, High, Moderate, Low, Warning, or Notice. |
priority | string | A priority from 1 to 5 (5 is highest), sent as text. |
message | string | A plain-language summary of the problem. |
recommended_techniques | array of strings | WCAG technique IDs (like G18) suggested for fixing it. |
assignee_name | string | Unassigned, or the name of whoever it’s assigned to. |
user_id | integer | null | The numeric ID of the assignee, if there is one. |
link | string | A direct link to view the issue in AAArdvark. |
payload | object | The raw output from the scanner. Useful bits include selector, context, boundingBox, and helpUrl. Handy for pinpointing and reproducing the problem; otherwise you can ignore it. |
created_at / updated_at | ISO-8601 string | When the issue was first logged, and last changed. |
previous_status, original_severity, original_failure | string | null | Filled in only when an issue is reclassified. |
data.type | string | Always "created" here. |
When an issue changes status
The issue.status_changed message arrives whenever an issue moves from one state to another – for example, when a team member marks it Resolved Pending, or when a scan confirms all its instances are fixed. The two fields data.previous_status and data.new_status tell you exactly what changed.
{
"version": "1.0",
"event": "issue.status_changed",
"timestamp": "2026-06-04T21:32:50.229250Z",
"site": { "id": 1428, "name": "site care", "url": null, "base_url": "https://sitecare.com", "workspace": { "id": 1, "name": "Abdulkhalek's workspace" } },
"data": {
"type": "status_changed",
"previous_status": "active",
"new_status": "resolved_pending",
"issue": {
"id": 2745,
"unique_site_issue_id": 1,
"status": "resolved_pending",
"...": "(same shape as issue.created — full issue object)"
}
}
}
| Field | Type | What does it tell you |
|---|---|---|
data.type | string | Always "status_changed". |
data.previous_status | string | Where the issue was before the change. Same set of values as issue.status. |
data.new_status | string | Where it is now. Matches data.issue.status. |
data.issue | object | The full issue, same shape as on a new-issue message. |
Two “previous status” fields – what’s the difference? data.previous_status is the status the issue had just before this particular change – the “from” side of the move you’re being told about. data.issue.previous_status is something else: it’s set when an issue was reclassified (say, its severity changed). The two often hold different values, so don’t treat them as the same thing.
When a new issue instance is found
An issue is the type of problem; an instance is one real occurrence of it on a page. The same issue can have many instances – one for every element that trips the same rule. This message (issue_instance.created) fires when a new one of those occurrences is logged.
{
"version": "1.0",
"event": "issue_instance.created",
"timestamp": "2026-06-04T21:32:50.232343Z",
"site": { "id": 1428, "name": "site care", "url": null, "base_url": "https://sitecare.com", "workspace": { "id": 1, "name": "Abdulkhalek's workspace" } },
"data": {
"type": "created",
"issue_instance": {
"id": 197377,
"unique_issue_instance_id": 1,
"site_id": 1428,
"page_id": 15209,
"issue_id": 2745,
"first_scan_id": 1271,
"scan_id": 1272,
"status": 0,
"ignored": 0,
"needs_review": 0,
"code": "color-contrast",
"runner": "axe",
"severity": "Very High",
"custom_severity": null,
"original_severity": null,
"converted": 0,
"issue_type": "automatic",
"selector": "#cp_popup_id_21429 > div:nth-child(2) > div > span",
"context": "<span class=\"cp-open-toggle-content\">Check My Site Health </span>",
"instance_details": "Fix any of the following:\n Element has insufficient color contrast of 4.07 (foreground color: #ffeefa, background color: #e00099, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1",
"note": "",
"steps_to_visible": "",
"test_mode": null,
"recommended_techniques": ["G18", "G145", "G148", "G174"],
"fingerprint": null,
"created_at": "2025-08-07T22:46:16.000000Z",
"updated_at": "2026-01-22T03:18:55.000000Z"
}
}
}
Inside data.issue_instance:
| Field | Type | What does it tell you |
|---|---|---|
id | integer | Our internal ID for this instance. |
unique_issue_instance_id | integer | The friendly per-issue number shown in the dashboard. |
site_id | integer | Which site is it on? |
page_id | integer | Which page does it appear on? |
issue_id | integer | The parent issue this is an example of. |
first_scan_id | integer | The scan that first found this exact occurrence. |
scan_id | integer | The most recent scan that saw it. |
status | integer | A numeric state: -1 Reopened, 0 Unfixed, 1 Fixed Pending, 2 Fixed. |
ignored | integer (0/1) | 1 if someone dismissed this occurrence. |
needs_review | integer (0/1) | 1 if it’s been flagged for a human to look at. |
code | string | The scanner’s code for the failure, e.g. color-contrast. |
runner | string | Which scanner caught it: axe, htmlcs, or ace. |
severity | string | Critical, Very High, High, Moderate, Low, Warning, or Notice. |
issue_type | string | automatic or manual. |
selector | string | A CSS selector pointing right at the offending element. |
context | string | The HTML of that element (trimmed to a sensible length). |
instance_details | string | The scanner’s plain explanation of why this one fails – e.g. the exact contrast ratio. |
note | string | Any free-text note someone added. Empty if none. |
steps_to_visible | string | Notes on how to make this element visible on the page (e.g., “click the menu first”). |
recommended_techniques | array of strings | Suggested WCAG technique IDs for fixing it. |
fingerprint | string | null | An internal de-duplication marker; not usually useful to you. |
created_at / updated_at | ISO-8601 string | When it was first logged and last changed. |
data.type | string | Always "created" here. |
When an instance changes status
This message (issue_instance.status_changed) arrives when one specific occurrence moves between states – for instance, when a scan notices a previously Unfixed element has now been fixed.
{
"version": "1.0",
"event": "issue_instance.status_changed",
"timestamp": "2026-06-04T21:38:25.883650Z",
"site": { "id": 1428, "name": "site care", "url": null, "base_url": "https://sitecare.com", "workspace": { "id": 1, "name": "Abdulkhalek's workspace" } },
"data": {
"type": "status_changed",
"previous_status": "0",
"new_status": "2",
"issue_instance": {
"id": 197377,
"status": 2,
"...": "(same shape as issue_instance.created — full issue_instance object)"
}
}
}
| Field | Type | What does it tell you |
|---|---|---|
data.type | string | Always "status_changed". |
data.previous_status | string | Where it was before, as the numeric code in quotes (e.g. "0", "2"). |
data.new_status | string | Where it is now, same quoted-number format. Matches data.issue_instance.status (which is sent as a plain number). |
data.issue_instance | object | The full instance, same shape as on a new-instance message. |
One small quirk to watch for: the instance status comes through as a number in data.issue_instance.status, but as a quoted string in data.previous_status and data.new_status. They mean the same thing either way – "-1" Reopened, "0" Unfixed, "1" Fixed Pending, "2" Fixed.
When a scan finishes
This message (scan.completed) lands when a scan wraps up – whether it succeeded or ran into trouble. It includes the full scan record, so you can see at a glance how many issues were added, resolved, or reopened.
{
"version": "1.0",
"event": "scan.completed",
"timestamp": "2026-06-04T21:32:50.237500Z",
"site": { "id": 1428, "name": "site care", "url": null, "base_url": "https://sitecare.com", "workspace": { "id": 1, "name": "Abdulkhalek's workspace" } },
"data": {
"type": "completed",
"scan": {
"id": 1271,
"workspace_id": 1,
"site_id": 1428,
"type": 0,
"status": 1,
"source": "Manual",
"batch_id": "9f949f36-164c-46d3-a16c-7be940300a9e",
"statistics": {
"errors": 11,
"warnings": 8,
"notices": 0,
"new_issues": 19,
"active_issues": 19,
"reopened_issues": 0,
"new_instances": 279,
"total_instances": 279,
"total_pages": 141,
"example_pages": []
},
"crawl_successes": 141,
"crawl_errors": 0,
"processing_errors": 0,
"started_at": null,
"finished_at": null,
"created_at": "2025-08-07T22:45:49.000000Z",
"updated_at": "2025-08-07T22:54:03.000000Z",
"message": null,
"page_ids_to_scan": null,
"chunk_offset": 0,
"total_pages_to_scan": 0
}
}
}
Inside data.scan:
| Field | Type | What does it tell you |
|---|---|---|
id | integer | Our internal ID for the scan. |
workspace_id | integer | The workspace it belongs to. |
site_id | integer | The site it ran against. |
type | integer | The scan type. 0 means a full whole-site scan; other values are partial scans. |
status | integer | 0 In Progress, 1 Completed, -1 Failed. A finished-scan message always carries 1 or -1. |
source | string | What kicked it off: Manual, Automated, and so on. |
batch_id | string (UUID) | An internal batch ID from our worker layer. |
statistics.errors | integer | How many error-level issues did this scan find? |
statistics.warnings | integer | How many warning-level issues? |
statistics.notices | integer | How many notice-level issues? |
statistics.new_issues | integer | Issues seen for the first time on this site. |
statistics.active_issues | integer | Issues active on the site when the scan finished. |
statistics.reopened_issues | integer | Previously resolved issues that turned up again. |
statistics.new_instances | integer | Occurrences found for the first time in this scan. |
statistics.total_instances | integer | All occurrences are active when the scan finishes. |
statistics.total_pages | integer | How many pages were crawled? |
crawl_successes | integer | Pages crawled successfully. |
crawl_errors | integer | Pages that couldn’t be crawled. |
processing_errors | integer | Internal hiccups while saving (usually 0). |
created_at | ISO-8601 string | When the scan was queued. |
updated_at | ISO-8601 string | When the record last changed (usually when it finished). |
data.type | string | Always "completed" here. |
Related Guides
-
Outgoing Webhooks
-
Setting Up an Outgoing Webhook
-
Verifying Webhook Signatures
-
Receiving Webhook Messages Reliably