Stream Issues
Stream audit issues in real-time using NDJSON
Stream Issues
/v1/audit/{job_id}/issues/streamStream audit issues in real-time as they are discovered. Uses NDJSON (Newline Delimited JSON) format for efficient streaming.
This endpoint is only available while the audit is running or after completion. For completed audits, it streams all issues at once.
When to Use Streaming
| Use Case | Recommendation |
|---|---|
| Display issues as they're found | Streaming |
| CI/CD pipelines | Polling (simpler) |
| Real-time dashboards | Streaming |
| Simple integrations | Polling |
| Large audits (100+ issues) | Streaming (lower memory) |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| job_id | string | required | The unique identifier returned when the audit was created. |
Response Format
The response uses application/x-ndjson content type. Each line is a complete JSON object followed by a newline character.
Issue Object
idstringUnique issue identifier.
severitystringenum: error | warning | infoIssue severity level.
categorystringenum: ux | accessibility | information_architecture | performanceIssue category.
rulestringRule identifier that triggered this issue.
messagestringHuman-readable description of the issue.
selectorstringCSS selector of the affected element (if applicable).
wcagstringWCAG criterion if this is an accessibility issue.
impactstringenum: critical | serious | moderate | minorImpact level on user experience.
fix_suggestionstringSuggested fix for the issue.
Example Stream
Each line is a valid JSON object. The stream looks like:
{"id":"issue_1","severity":"error","category":"accessibility","rule":"color-contrast","message":"Text has insufficient color contrast (3.2:1, minimum 4.5:1)","selector":"#hero-subtitle","wcag":"1.4.3"}
{"id":"issue_2","severity":"warning","category":"ux","rule":"touch-target-size","message":"Touch target is too small (32x32px, minimum 44x44px)","selector":".mobile-nav-toggle"}
{"id":"issue_3","severity":"info","category":"ux","rule":"button-text","message":"Button text could be more descriptive","selector":"#submit-btn","fix_suggestion":"Use 'Submit Form' instead of 'Submit'"}HTTP Status Codes
| Status | Description |
|---|---|
200 OK | Stream started successfully. |
401 Unauthorized | Missing or invalid API key. |
403 Forbidden | You don't have access to this audit. |
404 Not Found | Audit job not found. |
409 Conflict | Audit is still queued (not yet started). |
Code Examples
import 'dotenv/config';
interface Issue {
id: string;
severity: 'error' | 'warning' | 'info';
category: string;
rule: string;
message: string;
selector?: string;
wcag?: string;
impact?: string;
fix_suggestion?: string;
}
async function streamIssues(
jobId: string,
onIssue: (issue: Issue) => void
): Promise<void> {
const response = await fetch(
`https://vertaaux.ai/api/v1/audit/${jobId}/issues/stream`,
{
headers: {
'X-API-Key': process.env.VERTAAUX_API_KEY!,
'Accept': 'application/x-ndjson',
},
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Stream failed: ${error.message}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
// Process complete lines, keep incomplete line in buffer
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
const issue: Issue = JSON.parse(line);
onIssue(issue);
}
}
}
// Process any remaining data
if (buffer.trim()) {
const issue: Issue = JSON.parse(buffer);
onIssue(issue);
}
}
// Usage
let errorCount = 0;
let warningCount = 0;
await streamIssues('clu1a2b3c4d5e6f7g8h9i0j1', (issue) => {
console.log(`[${issue.severity.toUpperCase()}] ${issue.message}`);
if (issue.severity === 'error') errorCount++;
if (issue.severity === 'warning') warningCount++;
});
console.log(`\nTotal: ${errorCount} errors, ${warningCount} warnings`);# Stream issues line by line
curl -N "https://vertaaux.ai/api/v1/audit/$JOB_ID/issues/stream" \
-H "X-API-Key: $VERTAAUX_API_KEY" \
-H "Accept: application/x-ndjson"
# Process each issue with jq
curl -N "https://vertaaux.ai/api/v1/audit/$JOB_ID/issues/stream" \
-H "X-API-Key: $VERTAAUX_API_KEY" \
-H "Accept: application/x-ndjson" | \
while read -r line; do
echo "$line" | jq -r '"\(.severity | ascii_upcase): \(.message)"'
done
# Count issues by severity
curl -s "https://vertaaux.ai/api/v1/audit/$JOB_ID/issues/stream" \
-H "X-API-Key: $VERTAAUX_API_KEY" \
-H "Accept: application/x-ndjson" | \
jq -s 'group_by(.severity) | map({severity: .[0].severity, count: length})'import os
import json
import requests
from typing import Callable, TypedDict, Literal, Optional
class Issue(TypedDict):
id: str
severity: Literal['error', 'warning', 'info']
category: str
rule: str
message: str
selector: Optional[str]
wcag: Optional[str]
impact: Optional[str]
fix_suggestion: Optional[str]
def stream_issues(
job_id: str,
on_issue: Callable[[Issue], None]
) -> None:
"""Stream issues from an audit job."""
response = requests.get(
f'https://vertaaux.ai/api/v1/audit/{job_id}/issues/stream',
headers={
'X-API-Key': os.environ['VERTAAUX_API_KEY'],
'Accept': 'application/x-ndjson',
},
stream=True
)
if not response.ok:
error = response.json()
raise Exception(f"Stream failed: {error.get('message')}")
# Process stream line by line
for line in response.iter_lines(decode_unicode=True):
if line:
issue: Issue = json.loads(line)
on_issue(issue)
# Usage
issues_by_severity = {'error': 0, 'warning': 0, 'info': 0}
def handle_issue(issue: Issue) -> None:
print(f"[{issue['severity'].upper()}] {issue['message']}")
issues_by_severity[issue['severity']] += 1
stream_issues('clu1a2b3c4d5e6f7g8h9i0j1', handle_issue)
print(f"\nTotal: {issues_by_severity['error']} errors, "
f"{issues_by_severity['warning']} warnings")Real-Time Display Example
Build a real-time issue display for your dashboard:
import { useEffect, useState } from 'react';
function AuditIssueStream({ jobId }: { jobId: string }) {
const [issues, setIssues] = useState<Issue[]>([]);
const [status, setStatus] = useState<'streaming' | 'complete' | 'error'>('streaming');
useEffect(() => {
const controller = new AbortController();
async function stream() {
try {
const response = await fetch(
`/api/v1/audit/${jobId}/issues/stream`,
{
headers: { 'Accept': 'application/x-ndjson' },
signal: controller.signal,
}
);
const reader = response.body?.getReader();
if (!reader) throw new Error('No reader');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
const issue = JSON.parse(line);
setIssues(prev => [...prev, issue]);
}
}
}
setStatus('complete');
} catch (error) {
if (!controller.signal.aborted) {
setStatus('error');
}
}
}
stream();
return () => controller.abort();
}, [jobId]);
return (
<div>
<h2>Issues ({issues.length})</h2>
{issues.map(issue => (
<div key={issue.id} className={`issue-${issue.severity}`}>
<strong>{issue.severity}</strong>: {issue.message}
</div>
))}
{status === 'streaming' && <p>Streaming...</p>}
{status === 'complete' && <p>Stream complete</p>}
</div>
);
}NDJSON vs JSON
| Aspect | NDJSON (Streaming) | JSON (Polling) |
|---|---|---|
| Memory | Low (process line by line) | Higher (entire array) |
| Time to first issue | Immediate | After completion |
| Parsing | Per line | Entire response |
| Error recovery | Partial results possible | All or nothing |
| Browser support | ReadableStream API | Standard fetch |
Also Available
| Interface | Command/Method |
|---|---|
| CLI | vertaa audit --stream https://example.com |
| SDK | client.audits.streamIssues(jobId) |
Related
Was this page helpful?