Rex Automaton
All posts
Operations & Admin AutomationJune 22, 202610 min read

TrackTik API Integration: Incidents to Portal and Billing

We used TrackTik Events Subscriptions and the REST API to stream incident reports into a client portal and map billable activities to invoices for a mid sized guard firm.

By Jacky Lei

We built a TrackTik integration that takes guard incident reports as they happen, pushes them into a client facing portal, and turns billable events into invoice line items based on contract rules. This is for security and guard companies that want real time visibility for clients and clean billing without manual re keying. The guide below shows how it works and how to build it safely.

TrackTik API integration is the practice of using TrackTik's REST API, Events Subscriptions, and export channels to move operational data into your own systems for portals, analytics, and billing.

The problem it solves

Security operators often export incident PDFs or CSVs and then re type hours into billing. That forces back and forth email with clients, errors in invoices, and late reconciliations. TrackTik already captures the source of truth in the field. The missing piece is getting it into a portal clients can see and a billing layer that applies your contract rules automatically.

TaskManual processAutomated with TrackTik API
Client visibility to incidentsEmail PDFs weekly, ad hoc screenshotsLive portal list with search, filters, and attachments
Billing from incidents and patrolsRe key time from reports into invoicesMap report types to billable activities, auto add invoice lines
Stakeholder notificationsCC clients from email when something happensEvents Subscriptions post to webhook, notify just the right stakeholders
Backfills and auditsPull exports on demand and reconcile by handScheduled CSV pulls with immutable ledger and diff reports

Harvard Business Review reports that 81 percent of customers try to handle matters themselves before reaching out to a live representative. Source: https://hbr.org/2017/01/kick-ass-customer-service. A portal fed by TrackTik data meets that expectation while cutting support back and forth.

How the automation works

We rely on TrackTik's Events Subscriptions to push incident and report events to our middleware. The middleware enriches the payload with site and client context, writes an immutable ledger, renders the client portal UI, and posts matching billable activities to TrackTik's billing surface according to your configured contracts and preferences. For historical backfills or audits, we use CSV or XLSX exports from Operation Reports and Data Lab.

  • Events Subscriptions webhook: TrackTik calls our HTTPS endpoint in real time when new reports are created or updated. We verify the shared secret header and queue processing.
  • OAuth2 token handling: We obtain access tokens using OAuth2 and refresh them as needed. TrackTik refresh tokens are single use, so the middleware rotates them carefully.
  • Enrichment and rules: We resolve the site, post, and client account, apply contract rules, and compute billable activity quantities. The ledger stores the raw JSON and the derived line items.
  • Client portal: A Next.js UI reads from the ledger and shows incidents, attachments, and billable markers to the client with filters by site, severity, and time.
  • Billing sync: For qualified events, we post billable activities to TrackTik Billing so invoices can be generated from configured billing settings.

TrackTik incidents flow into a middleware engine, which enriches and writes a ledger, renders a client portal, and syncs billable activities to billing

Step-by-step: how to build it

1) Create an Events Subscription to your webhook

Configure an Events Subscription in TrackTik to fire on report and incident creation. Set a shared secret header for verification and point it at your HTTPS endpoint.

// Express webhook receiver
import express from "express";
import crypto from "crypto";
 
const app = express();
app.use(express.json({ limit: "2mb" }));
 
function verifySignature(req, secret) {
  const received = req.header("x-tracktik-signature");
  const computed = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(req.body))
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(received || ""), Buffer.from(computed));
}
 
app.post("/webhooks/tracktik", async (req, res) => {
  if (!verifySignature(req, process.env.TRACKTIK_WEBHOOK_SECRET || "")) {
    return res.status(401).send("bad sig");
  }
  // Queue for processing to avoid webhook timeouts
  await queue.enqueue("incident", { payload: req.body, receivedAt: Date.now() });
  res.status(202).send("queued");
});
 
app.listen(process.env.PORT || 3000);

Key gotcha: TrackTik can send bursts. Always queue and ack quickly to avoid retries.

2) Handle OAuth2 and single use refresh tokens

TrackTik uses OAuth2 with refresh tokens. Refresh tokens are single use. Obtain a new access token by POSTing to the access token endpoint with grant type refresh token, then store the returned refresh token for the next exchange.

import fetch from "node-fetch";
 
async function refreshToken({ domain, clientId, clientSecret, refreshToken }) {
  const url = `https://${domain}/rest/oauth2/access_token`;
  const resp = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      client_id: clientId,
      client_secret: clientSecret,
      refresh_token: refreshToken,
    }),
  });
  if (!resp.ok) throw new Error(`token refresh failed ${resp.status}`);
  const json = await resp.json();
  // Persist both tokens; do not reuse the old refresh token
  await tokenStore.save({
    accessToken: json.access_token,
    refreshToken: json.refresh_token,
    expiresAt: Date.now() + json.expires_in * 1000,
  });
  return json.access_token;
}

Gotcha: refresh once per process cycle. Parallel refreshes will burn otherwise valid refresh tokens.

3) Write an immutable incident ledger and enrichment

Store the raw webhook payload and a normalized record with site, post, and client. Use append only writes so audits are trivial.

-- incidents_ledger
CREATE TABLE incidents_ledger (
  id BIGSERIAL PRIMARY KEY,
  event_id TEXT NOT NULL,
  occurred_at TIMESTAMPTZ NOT NULL,
  client_id TEXT NOT NULL,
  site_id TEXT NOT NULL,
  severity TEXT,
  category TEXT,
  billable BOOLEAN DEFAULT FALSE,
  bill_code TEXT,
  quantity NUMERIC,
  raw JSONB NOT NULL
);
CREATE UNIQUE INDEX ON incidents_ledger (event_id);

Enrichment rule example: a Category of After hours dispatch maps to bill code ADH with quantity 1.0.

4) Build the client portal surface

Render the enriched records for authenticated client users with search and export. Keep attachments proxied from your store.

// Next.js server component sketch
import { sql } from "@vercel/postgres";
 
export default async function Incidents({ searchParams }) {
  const rows = await sql`
    SELECT occurred_at, site_id, severity, category, billable, bill_code, quantity
    FROM incidents_ledger
    WHERE client_id = ${searchParams.client}
    ORDER BY occurred_at DESC
    LIMIT 100
  `;
  return (
    <main>
      <h1>Incidents</h1>
      <table>
        <thead><tr><th>Date</th><th>Site</th><th>Category</th><th>Billable</th></tr></thead>
        <tbody>
          {rows.rows.map(r => (
            <tr key={r.occurred_at + r.site_id}>
              <td>{new Date(r.occurred_at).toLocaleString()}</td>
              <td>{r.site_id}</td>
              <td>{r.category}</td>
              <td>{r.billable ? `${r.bill_code} x ${r.quantity}` : "No"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

Design note: show billable markers in the portal so clients understand invoices before they arrive.

5) Post billable activities to TrackTik Billing

When a record is marked billable, post the activity to TrackTik Billing so it is available for invoicing under your configured billing preferences. Use your tenant's REST API surface documented under your portal domain.

// Wrapper function for billing sync, endpoint path is tenant documented
async function postBillableActivity({ domain, accessToken, payload }) {
  const url = `https://${domain}/rest/v1/...billing...`; // consult your tenant docs
  const resp = await fetch(url, {
    method: "POST",
    headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (!resp.ok) throw new Error(`billing sync failed ${resp.status}`);
  return resp.json();
}
 
// Example derived line
await postBillableActivity({
  domain: process.env.TRACKTIK_DOMAIN!,
  accessToken,
  payload: {
    client_id: rec.client_id,
    site_id: rec.site_id,
    code: rec.bill_code,
    quantity: rec.quantity,
    occurred_at: rec.occurred_at,
    memo: `Auto from incident ${rec.event_id}`,
  },
});

Gotcha: respect API limits. Default page size is 100 with a maximum of 1000 per request, and TrackTik can return 429 after heavy bursts.

6) Backfill or audit via CSV or XLSX exports

For history loads or audits, pull Operation Reports as CSV or XLSX, then merge into the ledger. CSV is recommended for large datasets.

# Python CSV ingest
import csv, psycopg
 
with open("operation_reports.csv", newline="") as f, psycopg.connect(dsn) as conn:
    rdr = csv.DictReader(f)
    with conn.cursor() as cur:
        for row in rdr:
            cur.execute(
              """
              INSERT INTO incidents_ledger(event_id, occurred_at, client_id, site_id, severity, category, billable, bill_code, quantity, raw)
              VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
              ON CONFLICT (event_id) DO NOTHING
              """,
              [row["ID"], row["Date"], row["Client"], row["Site"], row["Severity"], row["Category"], False, None, None, row]
            )

Note the bulk PDF export hard limit of 800 reports. Use CSV or XLSX for anything large.

Where it gets complicated

  • Refresh tokens are single use: TrackTik refresh tokens are consumed when used. If two workers refresh at once, one will fail and you can strand the session. Centralize refresh and cache access tokens with expiry.
  • Rate limits and timeouts: Default page size is 100, max 1000 per request, and about 4000 requests over five minutes can trigger 429. The gateway timeout is 30 seconds. Batch reads, implement backoff, and avoid long running queries on huge datasets.
  • CSV schema is fixed: Operation Reports CSV and XLS exports use fixed schemas. Do not rely on header renames to propagate. Version your parsers and add column presence checks.
  • PDF bulk limits: Bulk PDF exports cap at 800 reports. For auditors who want full archives, deliver zipped CSV plus a portal view, not a single PDF bundle.
  • Mobile draft nuances: On the mobile app, multiple report drafts cannot be kept simultaneously and navigating away can lose unsaved content. Expect the occasional partial and handle updates from Events Subscriptions.
  • Zapier or Make availability: TrackTik leans on API and webhooks. If there is no native app in your integrator, use HTTP and Webhooks modules to connect.

McKinsey estimates that in about 60 percent of occupations, at least 30 percent of activities could be automated. Source: https://www.mckinsey.com/featured-insights/employment-and-growth/a-future-that-works-automation-employment-and-productivity. Guard operations have a large administrative surface, so even a narrow incident to billing automation moves the needle.

What this actually changes

For a mid sized guard company, this removed manual re entry from incident logs to invoices and ended weekly PDF email threads with clients. In production, dispatch sees incidents in the portal seconds after submission, clients filter by site or severity, and billing has consistent line items tied to contract codes without spreadsheet gymnastics. The value is structural: the field captures once, and everything downstream updates by itself.

Frequently asked questions

Does TrackTik have an official API?

Yes. Each tenant exposes REST API documentation at your portal domain under rest v1. Authentication uses OAuth2 and the platform supports refresh tokens. We browse endpoints once authenticated at the tenant docs URL.

Can TrackTik push incidents to my system in real time?

Yes. Use Events Subscriptions to configure webhooks that call your HTTPS endpoint when report events occur. We verify a shared secret header and enqueue processing to avoid retries.

How do you handle rate limits and pagination?

TrackTik defaults to 100 items per page and allows up to 1000 per request, with about 4000 requests over five minutes risking a 429. We batch reads, back off on 429s, and avoid long running calls that could hit the 30 second gateway timeout.

What is the best way to load history and audits?

Use CSV or XLSX exports from Operation Reports or Data Lab for large backfills. PDF bulk exports cap at 800, and CSV has a fixed schema, so we version our parsers and deduplicate by a stable event ID.

Can this create invoice lines automatically?

Yes. We derive billable activities from incidents using your contract rules, then post those activities via the REST API so invoices can be generated from TrackTik Billing. We do not hard code endpoints. We follow the tenant documented billing surface.

Is there a Zapier or Make app for TrackTik?

TrackTik emphasizes API and webhooks. If a native app is not available in Zapier or Make, we integrate with their HTTP and Webhooks modules and keep the heavy lifting in our middleware.

If you want incidents flowing to a client portal and clean invoices without re keying, we have this running in production for a guard company and can adapt it to your contracts and sites. See our related post on bridging older security platforms in our SedonaOffice to QuickBooks write up at /blog/sedonaoffice-api-integration-to-quickbooks, explore more in our services under /services#custom-ai-integration, and if you want to scope your build, book a 15 minute call at /book.

Want us to build this for you?

15-minute discovery call. No pitch. We tell you what to automate first.

Book a Discovery Call

Related reading