Skip to main content
VertaaUX Docs
APIEndpoints

Stream Issues

Stream audit issues in real-time using NDJSON

Stream Issues

GET/v1/audit/{job_id}/issues/stream

Stream 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 CaseRecommendation
Display issues as they're foundStreaming
CI/CD pipelinesPolling (simpler)
Real-time dashboardsStreaming
Simple integrationsPolling
Large audits (100+ issues)Streaming (lower memory)

Path Parameters

ParameterTypeRequiredDescription
job_idstringrequiredThe 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

idstring

Unique issue identifier.

severitystringenum: error | warning | info

Issue severity level.

categorystringenum: ux | accessibility | information_architecture | performance

Issue category.

rulestring

Rule identifier that triggered this issue.

messagestring

Human-readable description of the issue.

selectorstring

CSS selector of the affected element (if applicable).

wcagstring

WCAG criterion if this is an accessibility issue.

impactstringenum: critical | serious | moderate | minor

Impact level on user experience.

fix_suggestionstring

Suggested 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

StatusDescription
200 OKStream started successfully.
401 UnauthorizedMissing or invalid API key.
403 ForbiddenYou don't have access to this audit.
404 Not FoundAudit job not found.
409 ConflictAudit 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

AspectNDJSON (Streaming)JSON (Polling)
MemoryLow (process line by line)Higher (entire array)
Time to first issueImmediateAfter completion
ParsingPer lineEntire response
Error recoveryPartial results possibleAll or nothing
Browser supportReadableStream APIStandard fetch

Also Available

InterfaceCommand/Method
CLIvertaa audit --stream https://example.com
SDKclient.audits.streamIssues(jobId)

Was this page helpful?

On this page