YipiiYipii IoT Docs

Reporting Service

Generate reports asynchronously with real-time WebSocket notifications

Share

The Reporting Service generates reports asynchronously. You submit a request, the report is queued and processed in the background, and you're notified via WebSocket when it's ready. This guide covers the full integration flow.

Architecture Overview

The reporting system uses an async-first design. You submit a request, the report is processed by background workers, and you receive a notification when it's ready.

Reporting service flow: Your App → Report Service → Background Worker → WebSocket Server → Your App

Step-by-Step Flow

Report lifecycle: Submit → Queued → Processing → Completed/Failed → Download

Reports are processed by background workers. Small reports may complete in seconds; larger ones (30 days of trail data) can take up to a minute.

Base URLs

ServiceURLPurpose
REST APIhttps://reporting.yipii.ioSubmit requests, check status, download
WebSocketwss://ws-reporting.yipii.ioReal-time completion notifications

Authentication

The Reporting Service uses the same OAuth2 Bearer token as the IoT API. Obtain a token via the Authentication guide, then include it in all requests:

Authorization: Bearer YOUR_ACCESS_TOKEN

Step 1: Submit a Report Request

Endpoint

POST https://reporting.yipii.io/api/v1/reports/generate

Parameters

ParameterTypeRequiredDescription
typestringYesReport type (see Report Types below)
formatstringYesOutput format: json, pdf, excel, or html
asset_idintegerYes*Asset ID to report on
asset_idsinteger[]NoMultiple asset IDs (for mileage reports)
beacon_idintegerNoBeacon ID (for beacon reports)
driver_idintegerNoDriver/employee ID
date_fromstringYesStart date (ISO 8601, e.g. 2026-01-01)
date_tostringYesEnd date (ISO 8601, e.g. 2026-01-31)
account_namestringNoAccount key (used for WebSocket channel routing)
titlestringNoCustom report title
prioritystringNolow, standard (default), or high

*asset_id is required for most report types. Beacon reports can use beacon_id instead.

JavaScript

const REPORT_API = 'https://reporting.yipii.io/api/v1';
 
async function submitReport(params) {
  const response = await fetch(`${REPORT_API}/reports/generate`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${ACCESS_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(params),
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || `HTTP ${response.status}`);
  }
 
  return response.json();
}
 
// Usage
const result = await submitReport({
  type: 'trip',
  format: 'pdf',
  asset_id: 123,
  date_from: '2026-01-01',
  date_to: '2026-01-31',
  account_name: 'acme-fleet',
  priority: 'standard',
});
 
console.log(`Report queued: ${result.report_id}`);

cURL

curl -X POST "https://reporting.yipii.io/api/v1/reports/generate" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "trip",
    "format": "pdf",
    "asset_id": 123,
    "date_from": "2026-01-01",
    "date_to": "2026-01-31",
    "account_name": "acme-fleet",
    "priority": "standard"
  }'

Python

import requests
 
REPORT_API = 'https://reporting.yipii.io/api/v1'
 
def submit_report(token, params):
    response = requests.post(
        f'{REPORT_API}/reports/generate',
        json=params,
        headers={'Authorization': f'Bearer {token}'}
    )
    response.raise_for_status()
    return response.json()
 
# Usage
result = submit_report(token, {
    'type': 'trip',
    'format': 'pdf',
    'asset_id': 123,
    'date_from': '2026-01-01',
    'date_to': '2026-01-31',
    'account_name': 'acme-fleet',
})
 
print(f"Report queued: {result['report_id']}")

Response (HTTP 201)

{
  "status": "success",
  "message": "Report queued for generation",
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "estimated_completion": "2026-01-31T10:00:10Z",
  "estimated_time_ms": 10000,
  "queue_position": 3,
  "queue_name": "reports_standard"
}

If an identical report was recently generated, the service returns it from cache immediately (HTTP 200) with "source": "cache" in the response.

Step 2: Connect to WebSocket

The Reporting Service broadcasts events via a Pusher-compatible WebSocket server. You can use any Pusher client library.

WebSocket channel routing: auth flow, channel subscription, event broadcast

Channel Format

private-reports.{accountName}.user.{userId}

For example, user 42 on account acme-fleet subscribes to:

private-reports.acme-fleet.user.42

Broadcasting Auth

Private channels require authentication. The client library handles this automatically by calling the auth endpoint when subscribing:

POST https://reporting.yipii.io/api/broadcasting/auth

With body:

{
  "channel_name": "private-reports.acme-fleet.user.42",
  "socket_id": "123456.789012"
}

The auth endpoint validates your Bearer token and returns a signature the WebSocket server uses to authorize the subscription.

JavaScript (Pusher JS)

import Pusher from 'pusher-js';
 
const pusher = new Pusher('yipii-reports', {
  wsHost: 'ws-reporting.yipii.io',
  wsPort: 443,
  wssPort: 443,
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
  disableStats: true,
  cluster: '',
  authEndpoint: 'https://reporting.yipii.io/api/broadcasting/auth',
  auth: {
    headers: {
      'Authorization': `Bearer ${ACCESS_TOKEN}`,
    },
  },
});
 
// Subscribe to your private channel
const channelName = `private-reports.${ACCOUNT_NAME}.user.${USER_ID}`;
const channel = pusher.subscribe(channelName);
 
channel.bind('pusher:subscription_succeeded', () => {
  console.log('Subscribed to report notifications');
});
 
channel.bind('pusher:subscription_error', (error) => {
  console.error('Subscription failed:', error);
});

Python (pysher)

import pysher
import json
 
pusher = pysher.Pusher(
    key='yipii-reports',
    custom_host='ws-reporting.yipii.io',
    secure=True,
    port=443,
    auth_endpoint='https://reporting.yipii.io/api/broadcasting/auth',
    auth_endpoint_headers={
        'Authorization': f'Bearer {token}'
    }
)
 
def on_connect(data):
    channel = pusher.subscribe(f'private-reports.{account_name}.user.{user_id}')
    channel.bind('.report.completed', on_report_completed)
    channel.bind('.report.failed', on_report_failed)
 
def on_report_completed(data):
    event = json.loads(data)
    print(f"Report ready: {event['report_id']}")
    print(f"Download: {event['file_url']}")
 
def on_report_failed(data):
    event = json.loads(data)
    print(f"Report failed: {event['error_message']}")
 
pusher.connection.bind('pusher:connection_established', on_connect)
pusher.connect()

Step 3: Listen for Events

Once subscribed, listen for these events on the channel:

Events

EventDescription
.report.queuedReport entered the processing queue
.report.processingWorker started generating the report
.report.completedReport is ready for download
.report.failedReport generation failed

Note the leading dot (.) — this is required by the server's event naming convention.

Completed Event Payload

{
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "completed",
  "type": "trip",
  "format": "pdf",
  "title": "Trip Report - Vehicle 123",
  "file_url": "https://...",
  "file_size": 1048576,
  "processing_time_ms": 8500,
  "started_at": "2026-01-31T10:00:00Z",
  "completed_at": "2026-01-31T10:00:08Z",
  "asset_info": {
    "id": 123,
    "name": "Vehicle 123"
  },
  "served_from_cache": false
}

Failed Event Payload

{
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "failed",
  "type": "trip",
  "format": "pdf",
  "error_message": "No data found for the specified date range",
  "asset_info": {
    "id": 123,
    "name": "Vehicle 123"
  }
}

JavaScript Event Handlers

// Listen for all report events
channel.bind('.report.completed', (data) => {
  console.log(`Report ${data.report_id} ready`);
 
  if (data.format === 'json') {
    // Fetch JSON data via status endpoint
    fetchReportData(data.report_id);
  } else {
    // Download PDF/Excel/HTML via download endpoint
    downloadReport(data.report_id);
  }
});
 
channel.bind('.report.failed', (data) => {
  console.error(`Report ${data.report_id} failed: ${data.error_message}`);
});
 
// Optional: track progress
channel.bind('.report.processing', (data) => {
  console.log(`Report ${data.report_id} is being generated...`);
});

Step 4: Download the Report

Once you receive a .report.completed event, fetch the result.

For PDF, Excel, HTML

Request a signed download URL:

GET https://reporting.yipii.io/api/v1/reports/{reportId}/download

Response

{
  "status": "success",
  "download_url": "https://sgp1.digitaloceanspaces.com/...",
  "file_size": 1048576,
  "file_size_human": "1 MB",
  "content_type": "application/pdf",
  "filename": "trip_vehicle-123_20260101_20260131.pdf",
  "expires_in_hours": 1,
  "expires_at": "2026-01-31T11:00:00Z"
}

The download_url is a pre-signed URL valid for 1 hour. Use it to download the file directly.

For JSON Format

Request the report data:

GET https://reporting.yipii.io/api/v1/reports/{reportId}/status

When the report is completed and the format is json, the status endpoint returns the data inline. Alternatively, use the download endpoint which returns the JSON content directly.

JavaScript

async function downloadReport(reportId) {
  const response = await fetch(
    `${REPORT_API}/reports/${reportId}/download`,
    {
      headers: { 'Authorization': `Bearer ${ACCESS_TOKEN}` },
    }
  );
 
  const result = await response.json();
 
  if (result.download_url) {
    // Open or fetch the signed URL
    window.open(result.download_url);
  }
}

cURL

# Get download URL
curl -X GET "https://reporting.yipii.io/api/v1/reports/{report_id}/download" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
 
# Download the file
curl -L -o report.pdf "DOWNLOAD_URL_FROM_RESPONSE"

Polling Fallback

If you can't use WebSocket (server-side scripts, restricted environments), poll the status endpoint instead:

GET https://reporting.yipii.io/api/v1/reports/{reportId}/status

Response

{
  "status": "success",
  "report_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "generation_status": "completed",
  "progress": 100,
  "type": "trip",
  "format": "pdf",
  "file_url": "https://...",
  "file_size": 1048576,
  "file_size_human": "1 MB",
  "processing_time_ms": 8500,
  "created_at": "2026-01-31T10:00:00Z",
  "completed_at": "2026-01-31T10:00:08Z"
}

JavaScript (Polling)

async function waitForReport(reportId, interval = 3000, timeout = 300000) {
  const start = Date.now();
 
  while (Date.now() - start < timeout) {
    const response = await fetch(
      `${REPORT_API}/reports/${reportId}/status`,
      {
        headers: { 'Authorization': `Bearer ${ACCESS_TOKEN}` },
        cache: 'no-store',
      }
    );
 
    const status = await response.json();
 
    if (status.generation_status === 'completed') {
      return status;
    }
 
    if (status.generation_status === 'failed') {
      throw new Error(status.error_message || 'Report generation failed');
    }
 
    await new Promise(resolve => setTimeout(resolve, interval));
  }
 
  throw new Error('Report generation timed out');
}
 
// Usage
const report = await submitReport({ type: 'trip', format: 'pdf', ... });
const completed = await waitForReport(report.report_id);
console.log(`Download: ${completed.file_url}`);

cURL (Polling)

# Check status (repeat every 3 seconds)
curl -X GET "https://reporting.yipii.io/api/v1/reports/{report_id}/status" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Complete Example

End-to-end JavaScript example using WebSocket with polling fallback:

import Pusher from 'pusher-js';
 
const REPORT_API = 'https://reporting.yipii.io/api/v1';
 
class ReportClient {
  constructor(token, accountName, userId) {
    this.token = token;
    this.accountName = accountName;
    this.userId = userId;
    this.headers = {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    };
    this.pusher = null;
    this.channel = null;
  }
 
  // Connect WebSocket
  connect() {
    this.pusher = new Pusher('yipii-reports', {
      wsHost: 'ws-reporting.yipii.io',
      wsPort: 443,
      wssPort: 443,
      forceTLS: true,
      enabledTransports: ['ws', 'wss'],
      disableStats: true,
      cluster: '',
      authEndpoint: 'https://reporting.yipii.io/api/broadcasting/auth',
      auth: { headers: { 'Authorization': `Bearer ${this.token}` } },
    });
 
    const channelName = `private-reports.${this.accountName}.user.${this.userId}`;
    this.channel = this.pusher.subscribe(channelName);
  }
 
  // Generate report and wait for result
  async generate(params) {
    // Submit the request
    const response = await fetch(`${REPORT_API}/reports/generate`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ ...params, account_name: this.accountName }),
    });
 
    const result = await response.json();
 
    if (result.source === 'cache') {
      return result; // Served from cache, already complete
    }
 
    // Wait for completion via WebSocket or polling
    return this.waitForCompletion(result.report_id);
  }
 
  waitForCompletion(reportId) {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        cleanup();
        reject(new Error('Report timed out after 5 minutes'));
      }, 300000);
 
      const cleanup = () => {
        clearTimeout(timeout);
        clearInterval(pollInterval);
        if (this.channel) {
          this.channel.unbind('.report.completed', onCompleted);
          this.channel.unbind('.report.failed', onFailed);
        }
      };
 
      // WebSocket listener
      const onCompleted = (data) => {
        if (data.report_id === reportId) {
          cleanup();
          resolve(data);
        }
      };
 
      const onFailed = (data) => {
        if (data.report_id === reportId) {
          cleanup();
          reject(new Error(data.error_message));
        }
      };
 
      if (this.channel) {
        this.channel.bind('.report.completed', onCompleted);
        this.channel.bind('.report.failed', onFailed);
      }
 
      // Polling fallback (runs alongside WebSocket)
      const pollInterval = setInterval(async () => {
        try {
          const res = await fetch(`${REPORT_API}/reports/${reportId}/status`, {
            headers: this.headers,
            cache: 'no-store',
          });
          const status = await res.json();
 
          if (status.generation_status === 'completed') {
            cleanup();
            resolve(status);
          } else if (status.generation_status === 'failed') {
            cleanup();
            reject(new Error(status.error_message));
          }
        } catch (e) {
          // Polling error, continue waiting
        }
      }, 3000);
    });
  }
 
  // Download completed report
  async download(reportId) {
    const response = await fetch(`${REPORT_API}/reports/${reportId}/download`, {
      headers: this.headers,
    });
    return response.json();
  }
 
  disconnect() {
    if (this.pusher) {
      this.pusher.disconnect();
    }
  }
}
 
// Usage
const client = new ReportClient(ACCESS_TOKEN, 'acme-fleet', 42);
client.connect();
 
try {
  const report = await client.generate({
    type: 'trip',
    format: 'pdf',
    asset_id: 123,
    date_from: '2026-01-01',
    date_to: '2026-01-31',
  });
 
  console.log('Report completed:', report.report_id);
 
  // Get the download URL
  const download = await client.download(report.report_id);
  console.log('Download:', download.download_url);
} finally {
  client.disconnect();
}

Report Types

TypeDescriptionMax Date Range
trailGPS position history with speed, ignition, movement30 days
tripTrip summaries with start/end locations, distance, duration30 days
driver_behaviorHarsh braking, acceleration, cornering events30 days
driver_scoreDriver performance scoring30 days
sensorTemperature, fuel level, and other sensor data30 days
fuelFuel consumption analysis30 days
alarms_dataAlert and alarm history30 days
obd_dataOBD-II vehicle diagnostics14 days
beacon_environmentBluetooth beacon detection events14 days
portable_beaconPortable beacon tracking (requires beacon_id)14 days
mileage_dailyMileage aggregated by day30 days
mileage_weeklyMileage aggregated by week90 days
mileage_monthlyMileage aggregated by month365 days
mileage_comparisonPeriod-over-period mileage comparison365 days

Output Formats

FormatContent TypeDescription
jsonapplication/jsonRaw data returned directly
pdfapplication/pdfFormatted PDF document
excelapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetExcel spreadsheet
htmltext/htmlHTML report

For json format, the data is returned directly in the response. For all other formats, you receive a pre-signed download URL valid for 1 hour.

Rate Limits and Constraints

ConstraintValue
Max concurrent reports per user3
Queue timeout10 minutes
Date range exceeding type maxReturns 400 error
Download URL validity1 hour

Error Handling

Common Error Responses

400 — Validation Error

{
  "status": "error",
  "message": "The type field is required.",
  "errors": {
    "type": ["The type field is required."]
  }
}

401 — Authentication Failed

{
  "message": "Unauthenticated."
}

403 — Access Denied

{
  "status": "error",
  "message": "You do not have access to this asset."
}

429 — Rate Limited

{
  "status": "error",
  "message": "Too many report requests. Please try again later."
}

503 — Service Degraded

When the queue is overloaded, the service enters degraded mode and rejects new requests temporarily:

{
  "status": "error",
  "message": "Service is temporarily overloaded. Please retry in 30 seconds."
}

Next Steps

Was this page helpful?

Reporting Service | Yipii IoT Docs