Dividia ScaleWatcher Public API
The ScaleWatcher Public API gives external integrators read access to ticket transactions, events, alerts, and downloadable PDF reports for sites your account has been granted access to.
Base URL: https://api.cloud.dividia.net
All public-API endpoints live under /sw/api/... and require a JWT bearer token (see Authentication). API access is enabled per-account by Dividia support — see Getting Started.
Getting Started
The Dividia ScaleWatcher API is not self-serve in v1 — API access is enabled manually by Dividia support after verifying contract status and site authorization.
1. Request API access
Email support@dividia.net with:
- Your existing ScaleWatcher Cloud account email
- The site serial(s) you want API access for (visible in your SW Cloud dashboard)
- A brief description of your intended integration
If you don't yet have a ScaleWatcher Cloud account, you'll need one first — your account credentials are also your v1 API credentials. Long-lived API keys distinct from account passwords are planned for a future release.
2. Wait for activation
Dividia support will verify your authorization and contract status for the requested site(s). Typically one business day during regular support hours (Monday–Friday, 8 AM – 5 PM Central, excluding US federal holidays).
3. Review and accept Terms of Service
Your activation email includes a link to the Dividia API Terms of Service. Reply confirming acceptance before making your first API call. (Click-to-accept is coming in a future release.)
4. Authenticate
curl -X POST https://api.cloud.dividia.net/sw/api/auth \
-H "Content-Type: application/json" \
-d '{"email":"you@your-company.com","password":"..."}'
The response includes a persistent JWT, your user info, and the list of sites you can query. Cache the JWT and reuse it for every request — your token does not expire automatically (see Token lifetime & rotation). The /auth endpoint is rate-limited to 10 requests per 15 minutes per IP, so don't re-authenticate on every call.
5. Confirm your sites
curl https://api.cloud.dividia.net/sw/api/sites \
-H "Authorization: Bearer $TOKEN"
6. Make your first data request
curl "https://api.cloud.dividia.net/sw/api/transactions?serial=YOUR_SERIAL&startDate=2026-06-01&endDate=2026-06-07" \
-H "Authorization: Bearer $TOKEN"
You're integrated. See the Endpoints reference below for the full API surface.
Transport security
We strongly recommend always using the https:// scheme directly. Plain-HTTP requests are redirected to HTTPS (HTTP 301) at the infrastructure layer — relying on the redirect can hide bugs in your client during development, so prefer to construct the URL with https:// from the start.
| Property | Value |
|---|---|
| TLS | TLS 1.0 and newer accepted; modern clients negotiate TLS 1.2 or 1.3 automatically |
| HSTS | Strict-Transport-Security: max-age=15552000; includeSubDomains |
| Certificate authority | Amazon Trust Services (via AWS Certificate Manager) |
If you're seeing unexplained connection errors, verify your client is using the https:// scheme. Most modern HTTP libraries default to TLS 1.3 automatically; older clients fall back to TLS 1.2 or earlier as needed.
Authentication
All public-API endpoints — with the single exception of POST /sw/api/auth — require a signed JWT bearer token in the Authorization header.
How to obtain a token
POST your account credentials to /sw/api/auth. The response contains a JWT, basic account info, and the sites your account is permitted to access. Cache the JWT — see Token lifetime & rotation below.
How to pass the token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
/sw/api/auth returns 403 ForbiddenRequest, contact Dividia (support@dividia.net or 866-348-4342) to enable API access.
Token lifetime & rotation
Your API token does not expire automatically. It remains valid until explicitly rotated:
- A Dividia admin rotates your underlying API key (manual action via the admin console)
- You request rotation via support (e.g., after a suspected leak)
We recommend rotating your token at least once a year, or immediately if you suspect exposure.
Rotation flow
To rotate your token, email support@dividia.net. The Dividia team will regenerate your underlying API key, which immediately invalidates the old token, and email you the replacement. Same-business-day turnaround during regular support hours (Monday–Friday, 8 AM – 5 PM Central).
Handling 401 responses
A persistent 401 on a previously-working token indicates the underlying key was rotated. Wait for the email containing your new token, or contact support if the email is delayed.
Storage
Treat your API token as a high-value secret. Store it server-side only — environment variables, AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, Azure Key Vault, or your equivalent. Never commit it to source control. Never expose it to browser-accessible code unless your origin has been explicitly approved by Dividia (see Browser-based integrations).
Browser-based integrations
In v1, the Dividia API is primarily a server-side API. By default it accepts cross-origin browser requests only from Dividia-owned domains (SW Cloud, NVR, etc.).
We recommend server-side integration. v1 API tokens grant the full access scope of the user that owns them. Storing a token in browser memory exposes it to anyone who can inspect the page (DevTools, XSS, compromised dependencies). A leaked token gives an attacker the same access the legitimate user has, until the token is rotated.
If your integration absolutely requires direct browser-to-API calls:
- Email support@dividia.net with your origin (scheme + host + port), the integration use case, and how the token will be protected from exposure.
- Origin requests are evaluated case-by-case and may require security review or additional safeguards before approval.
Server-side integrations (Node.js, Python, Java, Go, etc.) are unaffected — CORS only applies to browser-initiated cross-origin requests.
Browser-based integration becomes more practical in a future release when per-key scopes are introduced — a read-only scoped key in browser memory has much lower blast radius than a full-access key.
Rate limits
The Dividia API applies layered rate limits — per-IP for unauthenticated traffic protection, per-(user, site) for tenant fairness, per-site for backend protection.
| Limit | Cap | Window | Scope |
|---|---|---|---|
Authentication (POST /sw/api/auth) | 10 requests | 15 min | per IP |
| General API (authenticated) | 1,000 requests | 15 min | per IP |
| Per-(user, site) | 60 requests | 1 min | per (user, site) — each serial in a multi-site request counts independently |
| Per-site (across all users) | 60 requests | 1 min | per site — each serial in a multi-site request counts independently |
| Duplicate-request guard | identical params | 10 sec | per (user, site) — returns 429 TooManyRequests with message "Duplicate request" |
Response headers
Every response includes RFC-draft rate-limit headers describing your current budget against the published limits:
| Header | Meaning |
|---|---|
RateLimit-Limit | The applicable cap for this request. |
RateLimit-Remaining | Approximate number of requests remaining in the current window. |
RateLimit-Reset | Seconds until the window resets. |
Retry-After | On 429: seconds to wait before retrying. |
Headers use the unprefixed RateLimit-* naming from the IETF RFC draft — not the legacy X-RateLimit-* form.
/auth and /sites), the RateLimit-* headers on a 200 reflect the binding per-site cap (60 requests / minute) — so RateLimit-Remaining counts down toward the limit that actually throttles you, and you can self-pace against it. On /auth and /sites (which have no per-site limiter), the headers reflect the global per-IP cap (1,000 / 15 min). Every 429 also carries RateLimit-* plus Retry-After.
429 response shape
When you hit a limit, the response body uses the standard error envelope:
{
"error": {
"code": 429,
"type": "TooManyRequests",
"message": "Too many requests from this IP for site serial 4001"
}
}
Per-(user, site) and per-site 429s carry all four headers above. The duplicate-request 429 (10-second de-dup guard) carries only Retry-After — it's not a rate bucket but a "you have an identical request still in flight" signal.
Multi-site request accounting
A request with ?serial=2318,2401,4001 counts as one request against each site's per-site bucket (so each site's 60/min cap applies individually) but only one request against your global per-IP cap. Batching across sites you have access to saves IP-level budget.
Higher limits
Higher limits are available on request — contact support@dividia.net with your expected request volume and use case.
Conventions
Site serial
Every data and PDF endpoint (except /auth and /sites) requires a serial query parameter identifying which site to query. You can only query sites your account has been granted access to; the list of sites you can use comes back in the /auth and /sites responses.
Multi-site queries: serial also accepts a comma-list of up to 10 serials (e.g. ?serial=2318,2401,4001). The whole request fails with 403 if any supplied serial is outside your access — the response names the offending serial(s). Pagination spans the combined result set, ordered by primary timestamp DESC. Each row in the response carries its own serial field so you can demux client-side.
{
"error": {
"code": 403,
"type": "ForbiddenRequest",
"message": "Access denied for site serial(s): 9999, 8888",
"unauthorizedSerials": [9999, 8888]
}
}
The unauthorizedSerials array is machine-parseable; the message field is human-readable. If you typo'd a serial or you're not in the access list for a site you expected to be, this tells you which.
Site customer types
Each site has a customerType exposed on the /sites response. The value is one of three documented options, structured as an additive hierarchy:
truck— base tier: scale weighment events + ticketing. No license-plate recognition, no facility in/out tracking. ThefacilityDuration*aggregates on/transactions/metricsare zero for these sites;scaleDuration*andwaitDuration*may carry values from scale-event timestamps.lpn— truck plus license-plate-recognition events.LprReadevents appear in/eventsand populate the transactionlpnfield. Same facility-duration caveat astruck.facility— lpn plus facility entry/exit events and full facility-duration tracking. All three duration aggregates (facilityDuration*,scaleDuration*,waitDuration*) carry meaningful values.
Query /sites once at app boot to discover each site's type, then choose appropriate metrics. Cross-customer-type batched queries are allowed and may return zero values in inapplicable metric fields.
Pagination & ordering
All list endpoints accept these common query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | 1-based page number. |
pageSize | integer | 100 | Items per page. Defaults to 100 when omitted (or when given a non-positive value). The hard cap is 1,000 for non-PDF endpoints — a larger request is clamped down to 1,000; PDF endpoints cap at 50. |
orderBy | string | endpoint-specific | Field name to sort by. Must be a documented filter field on that endpoint (uses filter-param names, not response-field names). |
orderDir | string | ASC | ASC or DESC. |
413 PayloadTooLarge. Adequate for ~50 comma-listed ids + multiple pass-through filters; contact support if you bump into it.
Dates and time zones
Date parameters (date, startDate, endDate) are interpreted in the site's local time zone. Each site has its own tzOffset (UTC offset in hours), applied automatically when processing date parameters and when returning timestamps.
Response timestamps are ISO 8601 strings with the site's offset embedded, so they're unambiguous regardless of how your client interprets them.
Default time window & maximum range
To keep responses fast, list and metrics queries are always bounded by time:
- No date filter → last 90 days. If you supply none of
date,startDate,endDate, orsince(and you're not fetching a specific record — see below), the response covers the most recent 90 days in the site's local time. When this default is applied, the response carries anX-Dividia-Default-Window: 90dheader, so it's never a silent surprise. To reach further back, pass an explicit range. - Record lookups are exempt. A query that pins a specific record — by
idorticket(events alsotransactionId) — returns that record regardless of age; no default window is applied. - Maximum span: 366 days. An explicit
startDate/endDaterange — or asincereaching further back than that — wider than 366 days is rejected with400 DateRangeTooLarge. Split larger pulls into multiple requests.
Worked example
A Central Time site (UTC-5 during CDT, UTC-6 during CST — e.g., a Texas-based site) querying for June 10, 2026's transactions:
GET /sw/api/transactions?serial=2201&date=2026-06-10
Server interprets 2026-06-10 as the site's local calendar day:
2026-06-10 00:00:00 -05:00 → 2026-06-10 23:59:59 -05:00
(equivalent to UTC 2026-06-10 05:00 → 2026-06-11 05:00)
Response:
{
"data": [{
"id": 12345,
"serial": 2201,
"timestamp": "2026-06-10T14:30:00-05:00",
"tzOffset": -5,
...
}]
}
The timestamp string ("2026-06-10T14:30:00-05:00") is ISO 8601 with the site's offset embedded — any standard date parser handles this natively without your code needing to know the timezone separately.
Multi-site queries
When querying multiple sites (e.g., ?serial=2201,2301,2401), each row carries its own tzOffset because sites may be in different time zones. A single response can mix offsets:
[
{"serial": 2201, "tzOffset": -5, "timestamp": "2026-06-10T14:30:00-05:00"},
{"serial": 2301, "tzOffset": -6, "timestamp": "2026-06-10T13:30:00-06:00"},
{"serial": 2401, "tzOffset": -7, "timestamp": "2026-06-10T12:30:00-07:00"}
]
The three rows above represent the SAME UTC moment (19:30:00 UTC) in each site's local time.
Date-range parameters on multi-site queries are interpreted PER SITE. date=2026-06-10 against a Central-Time site and a Pacific-Time site each returns that site's local June 10th — same calendar label, different UTC windows.
Daylight saving time
tzOffset reflects the site's CURRENT offset including DST. A Texas site reports tzOffset: -5 in summer (CDT) and tzOffset: -6 in winter (CST). The transition happens automatically; you don't need to handle DST in your code.
Why site-local rather than UTC
The most common API question is "show me transactions for [calendar date]." Using UTC would mean a Texas site's 8 PM transaction on June 10 would belong to UTC June 11 — counterintuitive for operational queries.
If your integration needs UTC-based aggregation, convert the ISO 8601 timestamp using your standard date library — the offset is in the string.
Response shapes
List endpoints return JSON of this shape:
{
"page": 1,
"pageSize": 50,
"total": 1247,
"count": 50,
"data": [ ... ]
}
total— total matches across all pages.count— items returned in this response (≤pageSize).data— array of records for this page.
Field names in responses use camelCase and carry no storage prefixes — serial, ticket, startTime, facilityDuration, assignedUser. Booleans read as positive predicates (complete: true, flagged: true) — never negative ones. Timestamps are ISO 8601 in the site's local time zone unless noted otherwise.
Error envelope
All error responses share a consistent envelope:
{
"error": {
"code": 400,
"type": "BadRequest",
"message": "Invalid 'startDate' value"
}
}
Some errors include additional structured fields alongside the standard three — for example, 403 ForbiddenRequest may include unauthorizedSerials, and 403 on disabled-API-access may include apiDisabledSerials. See Common errors for branchable response fields, and Error code reference for the full status code list.
Every response from a data endpoint (transactions, events, alerts, /transactions/metrics, /transactions/pdf, /events/types, /events/keys) — success or error — also carries an X-Dividia-Request-Id response header. Capture this in your client logs; it identifies the exact server-side record for the request and is the fastest way for Dividia support to land on your specific issue when you email about it. See Reporting issues for the full template.
Endpoints (v1)
Nine endpoints. All reads. POST only on /auth; all others are GET.
| Method | Path | Purpose |
|---|---|---|
POST | /sw/api/auth | Exchange creds for a JWT |
GET | /sw/api/sites | List sites your account can access |
GET | /sw/api/transactions | List ticket transactions with filters + pass-through kv |
GET | /sw/api/transactions/metrics | Aggregate stats with dimension or time-bucket groupBy |
GET | /sw/api/transactions/pdf | Flexible filter → PDF, ZIP, or 0-match JSON |
GET | /sw/api/events | List individual events with filters + pass-through kv |
GET | /sw/api/events/types | Distinct event-type names seen at the site (recent window) |
GET | /sw/api/events/keys | Distinct JSON field names seen in event payloads (recent window) |
GET | /sw/api/alerts | List alerts (overweight, no-ticket, etc.) |
POST/sw/api/auth
Exchange email + password for a JWT bearer token. The only endpoint that does not require an existing token.
Rate limit: 10 requests per 15 minutes per IP. Cache your JWT and reuse it — your token does not expire automatically, so re-authenticating on every call is unnecessary and will exhaust the auth rate limit. See Token lifetime & rotation.
Request body
{
"email": "you@your-company.com",
"password": "your-account-password"
}
Response (200)
{
"token": "eyJhbGciOi...",
"user": {
"id": 123,
"email": "you@your-company.com",
"name": "Your Name"
},
"sites": {
"count": 3,
"data": [
{ "id": 42, "name": "Site A", "serial": 2318, "parentName": "Parent Co", "customerType": "facility" },
{ "id": 43, "name": "Site B", "serial": 2212, "parentName": "Parent Co", "customerType": "truck" },
{ "id": 44, "name": "Site C", "serial": 2401, "parentName": "Parent Co", "customerType": "facility" }
]
}
}
Example
curl -X POST https://api.cloud.dividia.net/sw/api/auth \
-H "Content-Type: application/json" \
-d '{"email":"you@your-company.com","password":"..."}'
GET/sw/api/sites
List sites your account is permitted to access. Same shape as the sites sub-object in /auth.
Parameters
None.
Response (200)
{
"count": 3,
"data": [
{ "id": 42, "name": "Site A", "serial": 2318, "parentName": "Parent Co", "customerType": "facility" },
...
]
}
The customerType field is always one of facility, truck, or lpn. See Site customer types for what each value means.
Example
curl https://api.cloud.dividia.net/sw/api/sites \
-H "Authorization: Bearer $TOKEN"
GET/sw/api/transactions
List ticket transactions for a site, with optional filters. Supports pass-through key/value filters on per-event JSON metadata.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
id | integer | optional | Internal transaction id. |
scale | integer | optional | Scale index within the site. |
lpn | string | optional | License plate. Best-effort: only populated when an LprRead event landed on the transaction. |
truck | string | optional | Truck id (from the site's ticketing integration). |
ticket | integer | optional | Loadout ticket number. |
date | date | optional | Exact day, YYYY-MM-DD. |
startDate | date | optional | Inclusive lower bound, YYYY-MM-DD. |
endDate | date | optional | Inclusive upper bound, YYYY-MM-DD. |
since | ISO 8601 datetime | optional | Exclusive lower bound on timestamp — return only items with timestamp > since. For incremental polling; see Polling patterns. Requires a timezone designator (Z or ±HH:MM). Mutually exclusive with date; composable with startDate/endDate. |
includeEvents | boolean | optional | 1 to nest per-transaction events. |
includeAlerts | boolean | optional | 1 to nest per-transaction alerts. |
| any other key | string | optional | Pass-through kv filter — matched against linked events' sData. Example: ?customer=ACME&product=Limestone. |
Response shape (per item in data)
{
"id": 12345,
"serial": 2318,
"scale": 1,
"ticket": 9999,
"lpn": "1N56558",
"truck": "T-PSA-1780511825",
"facilityDuration": 1800,
"scaleDuration": 600,
"waitDuration": 300,
"complete": true,
"timestamp": "2026-06-04T08:30:12-05:00",
"startTime": "2026-06-04T08:00:12-05:00",
"endTime": "2026-06-04T08:30:12-05:00",
"tzOffset": -6
}
scaleDuration field is the time (seconds) from the truck arriving on the scale to the configured transaction close-trigger. The close trigger is per-site — typically TicketReceived, ScaleTruckLeaving, or a configured alert event. May be zero on sites that close at TicketReceived, where the trigger can fire before or at the moment of scale arrival (the truck wasn't on the scale long enough to register a meaningful interval).
complete field is a confidence classification per transaction, primarily meaningful for customerType: facility sites. For facility sites, complete: true means the system saw a clean facility-entry → weighment → facility-exit flow within configured time thresholds. complete: false means the system couldn't observe the full sequence. Causes split into two categories: driver/flow reasons (drive-through, partial flow, abnormal duration) and site/hardware reasons (LPR camera offline or misaligned, unreadable plates, equipment moved or damaged). A spike in incomplete transactions at one site often signals a hardware or site-health issue, not driver behavior — worth investigating before treating it as a carrier-side problem. For truck and lpn sites, complete is always true — these tiers don't have the facility flow complete evaluates.
This endpoint does NOT filter by
complete — you get all transactions matching your filters regardless of completeness. If you only want completed loadouts (e.g., for reconciliation), filter client-side. The /transactions/metrics endpoint, by contrast, automatically excludes incomplete transactions (because aggregates over incomplete flows would be meaningless).
Examples
# Date range, with embedded events
curl "https://api.cloud.dividia.net/sw/api/transactions?serial=2318&startDate=2026-06-01&endDate=2026-06-07&includeEvents=1" \
-H "Authorization: Bearer $TOKEN"
# Pass-through kv — find transactions whose linked events have customer=ACME
curl "https://api.cloud.dividia.net/sw/api/transactions?serial=2318&customer=ACME" \
-H "Authorization: Bearer $TOKEN"
GET/sw/api/transactions/metrics
Aggregate counts and duration stats for transactions matching the supplied filters. Accepts every filter /sw/api/transactions accepts (including pass-through kv) and adds the groupBy parameter to slice by time bucket or dimension.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
groupBy | enum | optional | One of daily, hourly, carrier, customer, product, scale. Omit for a single summary row. |
Plus all filters from /sw/api/transactions (date range, lpn, ticket, scale, pass-through kv). | |||
Response shape
# groupBy=daily
{
"data": [
{ "date": "2026-06-01", "count": 412, "facilityDurationAvg": 405, "scaleDurationAvg": 200, "waitDurationAvg": 50, ... },
{ "date": "2026-06-02", "count": 388, "facilityDurationAvg": 412, ... }
]
}
# groupBy=carrier
{
"data": [
{ "carrier": "ACME Logistics", "count": 1247, "facilityDurationAvg": 410, ... },
{ "carrier": "Globex Freight", "count": 983, "facilityDurationAvg": 387, ... }
]
}
# no groupBy → array of one summary row
{
"data": [
{ "count": 2630, "facilityDurationAvg": 405, "scaleDurationAvg": 195, "waitDurationAvg": 52, ... }
]
}
count here is completed transactions only. /transactions/metrics excludes in-progress flows (a half-finished facility cycle has no meaningful durations), so on an active site this count is lower than the raw row count from /sw/api/transactions for the same filters. Query /sw/api/transactions directly if you need every row, including in-progress ones.
Examples
# Loads per carrier this month
curl "https://api.cloud.dividia.net/sw/api/transactions/metrics?serial=2318&startDate=2026-06-01&endDate=2026-06-30&groupBy=carrier" \
-H "Authorization: Bearer $TOKEN"
# Daily volume by product, last week
curl "https://api.cloud.dividia.net/sw/api/transactions/metrics?serial=2318&startDate=2026-06-01&endDate=2026-06-07&groupBy=daily&product=Limestone" \
-H "Authorization: Bearer $TOKEN"
GET/sw/api/transactions/pdf
Renders one or more transactions as PDFs. Returns a single PDF (exactly one match), a ZIP of PDFs (more than one match), or a JSON explanation (zero matches). Accepts the same filter parameters as /sw/api/transactions, plus comma-list shortcuts for id and ticket.
First-class structured filters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
id | integer or comma-list | optional | Single id or up to 50 ids comma-listed: id=12345 or id=12345,12346,12347. |
ticket | integer or comma-list | optional | Single ticket or up to 50 tickets comma-listed. |
ticketMin + ticketMax | integer pair | optional | Inclusive contiguous ticket range. Must be supplied together. |
date | date (YYYY-MM-DD) | optional | Single calendar day in site-local time. |
startDate + endDate | date pair | optional | Inclusive date range. Must be supplied together. |
| any other key | string | optional | Pass-through kv — matches per-event JSON metadata. |
Paging & image options
| Name | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | 1-based page number for the underlying transaction list. |
limit | integer | 25 | Per-page transaction count. Hard max 50. |
excludeImages | boolean | 0 | 1 to skip embedded images. |
Responses
| Match count | Status | Content-Type | Body |
|---|---|---|---|
| 1 match | 200 | application/pdf | Single PDF. Filename: <serial>-ticket_<n>.pdf if a ticket number is present, else <serial>-transaction_<id>.pdf. |
| >1 matches | 200 | application/zip | ZIP archive of PDFs. Outer filename: <serial>-transactions_<timestamp>.zip.Inner filenames follow the single-PDF convention. |
| 0 matches | 200 | application/json | JSON explanation (below) — distinguishes "unknown key" vs "value didn't match". |
Zero-match response shape
{
"matches": 0,
"filter": {
"serial": 2318,
"carrier": "ACME",
"dispatcher": "jane",
"startDate": "2026-06-01",
"endDate": "2026-06-07"
},
"keyNeverSeen": ["dispatcher"],
"valuesDidNotMatch": ["carrier"],
"message": "No transactions matched the supplied filters. dispatcher has never appeared in any event for this site — your integration may store it under a different key name. carrier has been seen but no event matched the supplied value."
}
Examples
# Exact ticket → single PDF
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticket=37237908" \
-H "Authorization: Bearer $TOKEN"
# Multiple ticket numbers → ZIP
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticket=37237908,37237909,37237910" \
-H "Authorization: Bearer $TOKEN"
# Multiple transaction ids → ZIP
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&id=12345,12346,12347" \
-H "Authorization: Bearer $TOKEN"
# Date range with pass-through kv filter
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&startDate=2026-06-01&endDate=2026-06-07&customer=ACME" \
-H "Authorization: Bearer $TOKEN"
# Skip images for smaller download
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticket=37237908&excludeImages=1" \
-H "Authorization: Bearer $TOKEN"
GET/sw/api/events
List individual events recorded on transactions. Events carry per-event JSON metadata (LPR reads, ticket data from your ticketing system, measurement events, etc.). Supports pass-through kv filters against sData.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
id | integer | optional | Event id. |
transactionId | integer | optional | Filter to one transaction. |
name | string | optional | Event name (e.g. LprRead, TicketReceived). Use /events/types to discover valid values. |
ticket | integer | optional | Loadout ticket number. |
date / startDate / endDate | date | optional | Date filtering. |
since | ISO 8601 datetime | optional | Exclusive lower bound on timestamp. For incremental polling; see Polling patterns. |
| any other key | string | optional | Pass-through kv — matched against sData. Example: ?name=TicketReceived&customer=ACME finds all TicketReceived events with customer ACME. |
Response shape (per item in data)
{
"id": 8401,
"serial": 2318,
"transactionId": 12345,
"ticket": 9999,
"name": "LprRead",
"data": { "lpn": "1N56558", "confidence": 0.97, "camera": "cam-1", "region": "main", "time": "2026-06-04T08:30:12Z" },
"images": [ "https://api.cloud.dividia.net/uploads/..." ],
"timestamp": "2026-06-04T08:30:12-05:00"
}
The images array is pre-signed for direct fetch — see Image URLs.
Examples
# All LprReads for a site
curl "https://api.cloud.dividia.net/sw/api/events?serial=2318&name=LprRead" \
-H "Authorization: Bearer $TOKEN"
# Ticket events for one customer
curl "https://api.cloud.dividia.net/sw/api/events?serial=2318&name=TicketReceived&customer=ACME" \
-H "Authorization: Bearer $TOKEN"
GET/sw/api/events/types
Distinct name values seen in the site's events within the query window. Use this to discover valid values for the name filter on /events. Like the other list endpoints, this scans a recent window rather than all of history: with no date filter it covers the most recent 90 days (the response carries an X-Dividia-Default-Window: 90d header); pass date, startDate+endDate, or since to shift or widen it (max span 366 days).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
category | enum | optional | alert returns only event types whose name starts with Alert (AlertOverweight, AlertNoTicket, etc.). non-alert returns the complement. Omit for all event types. |
date / startDate+endDate / since | date / datetime | optional | Restrict discovery to a time window. Default: last 90 days; max span 366 days. Same semantics as the time filters on /events. |
Response (200)
{
"count": 10,
"data": ["AlertNoTicket", "FacilityIn", "FacilityOut", "LprRead", "ScaleTruckEntering", "ScaleTruckLeaving", "ScaleTruckOn", "TicketReceived", "ValveClosed", "ValveOpened"]
}
Examples
# Alert types only — useful for building alert-dashboard filter lists
curl "https://api.cloud.dividia.net/sw/api/events/types?serial=2318&category=alert" \
-H "Authorization: Bearer $TOKEN"
# Non-alert events (tickets, LPR reads, scale events, etc.)
curl "https://api.cloud.dividia.net/sw/api/events/types?serial=2318&category=non-alert" \
-H "Authorization: Bearer $TOKEN"
Response is cached server-side with Cache-Control: private, max-age=3600.
GET/sw/api/events/keys
Distinct JSON field names seen at the top level of event data (or one level under data.ticket for TicketReceived payloads) within the query window. Use this to discover valid pass-through kv filter keys for /events, /transactions, /transactions/metrics, and /transactions/pdf. Scans the most recent 90 days by default (response carries X-Dividia-Default-Window: 90d); pass date, startDate+endDate, or since to shift or widen it (max span 366 days).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
eventName | string | optional | Narrow to keys seen in one or more specific event types. Comma-list, max 20 entries. Example: ?eventName=AlertOverweight or ?eventName=AlertOverweight,AlertNoTicket. Useful for discovering payload structure of a specific event type. |
category | enum | optional | alert narrows to keys seen in any Alert-prefixed event. non-alert is the complement. Composes with eventName via AND (intersection). |
date / startDate+endDate / since | date / datetime | optional | Restrict discovery to a time window. Default: last 90 days; max span 366 days. |
Examples
# All keys for one specific alert type
curl "https://api.cloud.dividia.net/sw/api/events/keys?serial=2318&eventName=AlertOverweight" \
-H "Authorization: Bearer $TOKEN"
# Keys across all alerts (broad alert-payload discovery)
curl "https://api.cloud.dividia.net/sw/api/events/keys?serial=2318&category=alert" \
-H "Authorization: Bearer $TOKEN"
Response (200)
{
"count": 13,
"data": [
{ "key": "lpn", "seenIn": ["LprRead"], "events": 84012 },
{ "key": "confidence", "seenIn": ["LprRead"], "events": 84012 },
{ "key": "ticket", "seenIn": ["TicketReceived"], "events": 12041 },
{ "key": "customer", "seenIn": ["TicketReceived"], "events": 12041 },
{ "key": "carrier", "seenIn": ["TicketReceived"], "events": 12041 },
{ "key": "product", "seenIn": ["TicketReceived"], "events": 12041 },
{ "key": "truck", "seenIn": ["TicketReceived"], "events": 12041 }
]
}
Sorted by frequency (most-common first). Response cached Cache-Control: private, max-age=3600.
GET/sw/api/alerts
List alerts (overweight, no-ticket, etc.) raised against transactions on this site.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
serial | integer | required | Site serial. |
id | integer | optional | Alert id. |
name | string | optional | Alert type (e.g. AlertOverweight, AlertNoTicket). |
transactionId | integer | optional | Filter to one transaction. |
status | string | optional | Alert status (open, acknowledged, etc.). |
flagged | boolean | optional | 1 for flagged-only. |
priority | enum | optional | low, medium, or high. |
hasComment | boolean | optional | 1 to limit to commented alerts. |
comment | string | optional | Substring search across comment text. |
assignedUserId / assignee | integer / string | optional | Filter by assignment (user id or username). |
date / startDate / endDate | date | optional | Date filtering. |
since | ISO 8601 datetime | optional | Exclusive lower bound on timestamp. For incremental polling; see Polling patterns. |
includeHistory | boolean | optional | 1 to nest the alert's history audit log — chronological entries for comments, assignments, and status changes by operator. |
Response shape (per item in data)
{
"id": 8421,
"serial": 2318,
"transactionId": 12345,
"name": "AlertOverweight",
"status": "open",
"flagged": true,
"assignedUserId": 7,
"assignedUser": "Alice Operator",
"priority": "high",
"data": { "ticket": 9999, "weighedNet": 95000, "permittedNet": 80000 },
"images": [ "https://api.cloud.dividia.net/uploads/scalewatcher/api/2318/2026/06/04/cam1-0.jpg?sig=...&exp=..." ],
"timestamp": "2026-06-04T08:30:12-05:00"
}
The images array is pre-signed for direct fetch — see Image URLs.
When ?includeHistory=1 is supplied, each alert also carries a history array of audit-log entries (comments, assignments, status changes):
"history": [
{ "id": 1, "alertId": 8421, "timestamp": "2026-06-04T08:35:00-05:00", "userId": 7, "user": "Alice Operator", "comment": "Investigating", "action": "comment" },
{ "id": 2, "alertId": 8421, "timestamp": "2026-06-04T09:00:00-05:00", "userId": 7, "user": "Alice Operator", "comment": "", "action": "status changed to: acknowledged" }
]
Examples
# All open overweight alerts for a site
curl "https://api.cloud.dividia.net/sw/api/alerts?serial=2318&name=AlertOverweight&status=open" \
-H "Authorization: Bearer $TOKEN"
# Alerts with their operator history
curl "https://api.cloud.dividia.net/sw/api/alerts?serial=2318&includeHistory=1" \
-H "Authorization: Bearer $TOKEN"
Pass-through key/value filters
Four endpoints accept arbitrary key/value filters that match against per-event JSON metadata: /transactions, /transactions/metrics, /transactions/pdf, and /events. This lets you filter on fields that integrations produce (customer, carrier, product, dispatcher, etc.) without requiring API changes when new fields show up.
How matching works
Any query parameter that isn't a documented first-class filter is treated as a pass-through kv pair. The matcher checks both the top-level of the event sData blob and one level under sData.ticket (because TicketReceived events nest most of their payload under .ticket). So ?customer=ACME finds events that have either sData.customer = "ACME" or sData.ticket.customer = "ACME".
Multiple kv pairs are AND'd — a record must match every supplied kv pair to be included.
Discovering valid keys
Use GET /sw/api/events/keys to list every distinct key name that has ever appeared in event metadata for the site. The response includes which event types each key appears in, so you can pick keys that match the kind of events you're filtering for.
Key name validation
Key names must match [a-zA-Z][a-zA-Z0-9_]{0,63} — starts with a letter, ASCII alphanumerics + underscore only, max 64 characters. Anything else returns 400 BadRequest. Values can be any string (URL-encode as needed).
When zero matches come back
For list endpoints (/transactions, /events, /transactions/metrics), zero matches just produce an empty data array.
For /transactions/pdf, zero matches return a 200 JSON response with keyNeverSeen and valuesDidNotMatch fields that distinguish "you used a wrong key name" from "the value didn't match anything." Use this for debugging.
Polling patterns
The Dividia API is polling-based in v1 — there are no webhooks. Integrators retrieve new data by calling /transactions, /events, or /alerts on a regular cadence.
Recommended pattern
- Maintain a watermark — the
timestampof the most recent item you've processed. - On each poll, query with
?since=<watermark>to fetch only items newer than your watermark. - Update the watermark to the maximum
timestampin the response. - On
429 TooManyRequests, honor theRetry-Afterheader. - Persist the watermark across restarts so you don't reprocess history after a deploy.
The since parameter accepts an ISO 8601 datetime with a required timezone designator (Z or ±HH:MM). It uses exclusive comparison (timestamp > since), so passing your last-seen timestamp back returns the next batch with zero overlap — no client-side dedup required.
# First poll — no watermark yet
GET /sw/api/transactions?serial=2318
→ 200, response.data has rows up to timestamp "2026-06-11T14:35:00.000-05:00"
# Subsequent polls — pass the prior max timestamp as `since`
GET /sw/api/transactions?serial=2318&since=2026-06-11T14:35:00.000-05:00
→ 200, response.data has only rows strictly newer than the watermark
Cadence
| Use case | Recommended cadence |
|---|---|
| Near-real-time dashboard | 1–2 min |
| Analytics / reporting | 15–60 min |
| Daily reconciliation | Once daily |
| Historical backfill | Use startDate + endDate with pagination — no cadence |
The published rate limits comfortably accommodate 1-minute cadence across multiple endpoints and sites. Multi-site batched queries (?serial=2318,2401,4001) reduce request count further — one call covers multiple sites' transactions.
since vs startDate
Use since for incremental polling — second-resolution, exclusive lower bound, no overlap with prior polls. Requires a precise timestamp; don't use it for "show me yesterday."
Use startDate + endDate for date-bounded queries — day resolution in the site's local time zone, inclusive boundaries. Right tool for "show me all of yesterday" or historical backfill.
since and date are mutually exclusive (400 BadRequest if both supplied) — they're different mental models. since composes with startDate/endDate via AND if you need both an incremental watermark and a hard ceiling.
Webhooks
Webhook-based delivery is not part of v1. We may implement webhooks in a future release based on integrator demand. Until then, the polling pattern above is the recommended approach.
Image URLs
Events and alerts may include image attachments. The images array in the response contains URLs pre-signed with a 24-hour HMAC signature.
- Embedding in browsers: the URLs work in
<img>tags directly — noAuthorizationheader required. The signature in the URL handles auth. - Re-fetching after expiry: when a signed URL expires (24 hours after issue), re-fetch the parent event or alert via
/eventsor/alertsto get fresh URLs. Don't try to extend the signature client-side — the parent endpoint always re-signs. - Server-side bulk download: if you'd prefer to use the
Authorizationheader instead of relying on the URL signature (e.g., for server-side scripts where signed URLs feel awkward), strip the?sig=&exp=query string and useAuthorization: Bearer <token>on the bare URL.
Image data itself is retained indefinitely (see Data retention). The 24-hour signature expiry limits the lifetime of any particular URL string, not the underlying image — refetch the parent record for fresh signed URLs whenever you need them.
Data retention
Dividia retains your ScaleWatcher data — transactions, events, alerts, and associated images — indefinitely by default. You can query historical data as far back as your site has been recording.
We reserve the right to introduce formal retention windows in a future release if operational requirements change. Any such change follows the standard deprecation policy — minimum 12-month notice with email notification to integrators using affected endpoints.
If you need data deleted (e.g., contract termination, data-protection request), contact support@dividia.net. Customer-initiated data deletion is handled out-of-band and is not part of the API surface.
Versioning & forward compatibility
The current API is implicit v1 — there is no version segment in the URL and no version header required. All integrators calling the API today are using v1.
Additive changes (no version bump)
The following kinds of changes ship without notice, version bump, or deprecation header:
- New endpoints
- New optional query parameters on existing endpoints
- New fields in response objects
- New event types (
namevalues returned by/events) - New error codes for previously-non-erroring conditions
- New values in existing
category/groupBy/ enum sets
Your client must accept these gracefully. Specifically:
- Ignore unknown fields in response objects rather than erroring.
- Treat unknown enum values as opaque strings rather than crashing.
- Don't pin to exact JSON shape; pin to the fields you actually use.
This is industry-standard "be lenient in what you accept" guidance (Postel's law) — every B2B API ships additive changes regularly, and strict-parsing clients break on every release.
Breaking changes (12-month notice)
Breaking changes require a minimum 12-month deprecation notice before removal. Breaking is defined as:
- Removing an endpoint or a response field
- Changing the semantics or data type of an existing field
- Adding a required parameter to an existing endpoint
- Removing or renaming an enum value
- Changing default behavior (e.g., default
pageSize) - Tightening rate-limit thresholds downward
- Tightening validation that was previously permissive
When we deprecate something, you'll see:
- A changelog entry with the target removal date (≥ 12 months out)
- A
Sunset: <RFC 7231 date>response header on the deprecated endpoint - A
Deprecation: trueresponse header on the deprecated endpoint - Email notification to integrators whose accounts called the affected endpoint in the prior 90 days
- A migration guide linked from the changelog and the endpoint's doc section
Future API versions
When v2 arrives, it'll be opt-in via an X-Dividia-API-Version request header with a date-based version identifier (e.g., 2027-03-15). v1 will continue serving as the default for at least 12 months after v2 announcement — you won't be forced to migrate.
Same URL serves both versions during the parallel-run window; the header switches behavior server-side.
Status and availability
Dividia is preparing a public status page for the API. Once live, it'll show current operational status for the public API and related services, incident history, and scheduled-maintenance announcements. You'll be able to subscribe to updates via email or RSS from the status page itself.
Dividia targets best-effort availability for the public API. We do not publish a contractual SLA in v1. Enterprise customers requiring specific availability guarantees should discuss SLA terms with Dividia sales.
For urgent operational issues (production outages affecting your integration), email support@dividia.net. For non-urgent status reports, the status page will be the appropriate channel once published.
Common errors and how to fix them
Most errors include structured response fields beyond the standard {code, type, message} envelope. Branching on these fields is more reliable than parsing the human-readable message.
400 BadRequest
Invalid parameter shape or value. The message field names the offending parameter.
| Common cause | Typical fix |
|---|---|
Missing required serial | Add ?serial=<your_site_serial>. |
Malformed date (e.g. 2026/06/10) | Use ISO 8601 calendar date: YYYY-MM-DD. |
| Invalid kv key (special character, starts with digit, >64 chars) | See key-name validation rules. |
| Unknown query parameter | Check the parameter list for the endpoint; remove unrecognized keys (they're rejected, not silently ignored). |
401 UnauthorizedAccess
Missing, malformed, or unrecognized bearer token. Re-check the Authorization header format.
| Common cause | Typical fix |
|---|---|
| Header missing entirely | Add Authorization: Bearer <your_token>. |
| Persistent 401 on previously-working token | Token rotated (your underlying key was regenerated). Wait for the replacement email from Dividia support, or contact support if not received. |
| Token doesn't decode | Re-fetch a fresh token from POST /sw/api/auth. Watch for trailing whitespace or truncation when copying. |
403 ForbiddenRequest
Authenticated, but not permitted. Response includes structured fields naming the specific failure:
| Field | Meaning | Fix |
|---|---|---|
unauthorizedSerials | Listed serials are outside your access allowlist. | Confirm the serial via GET /sw/api/sites. Contact support to grant access. |
apiDisabledSerials | Site exists in your access list but API access is not enabled for that site. | Contact support@dividia.net with the serial(s) to enable API access. |
unknownSerials | Serial doesn't exist in the Dividia database. | Verify the serial number; typos are the usual cause. |
| (no structured fields) | Your account doesn't have API access turned on at all. | Contact support to enable API access on your account. |
404 NotFound
The requested resource doesn't exist. Note that /sw/api/transactions/pdf intentionally returns 200 with a matches: 0 JSON body instead of 404 — that response shape carries diagnostic info (keyNeverSeen, valuesDidNotMatch) you can act on.
413 PayloadTooLarge
Query string exceeded 4,096 characters. Trim filters, reduce the size of comma-listed values, or split into multiple requests.
429 TooManyRequests
You've hit one of the documented rate limits.
| Message starts with | Meaning | Fix |
|---|---|---|
Duplicate request | You sent an identical request while a previous one was still being processed. | Wait Retry-After seconds (typically 10) and retry. Or de-duplicate client-side. |
Too many requests from this IP for site serial <N> | Per-(user, site) limit hit — 60 requests per minute for that serial. | Honor the Retry-After header. Reduce polling cadence. Use multi-site batched queries. |
Too many requests for site serial <N> | Per-site limit hit (across all users) — 60 requests per minute for that serial. | Honor Retry-After. Coordinate with other users querying the same site. Contact support if you need higher limits. |
Too many login attempts | You hit the /auth rate limit — 10 attempts per 15 minutes per IP. | Cache your token instead of re-authenticating on every request. Tokens don't expire automatically. |
500 InternalServerError
Server error. Retry with exponential backoff. If persistent, contact support@dividia.net with the timestamp, the request URL, and the response body.
Error code reference
| HTTP | type | When |
|---|---|---|
400 | BadRequest | Invalid or missing parameter. The message field names the offending parameter. |
400 | DateRangeTooLarge | Explicit date range (or since lookback) exceeds the 366-day maximum. Split into multiple requests. See Dates and time zones. |
400 | QueryTooBroad | Query too broad to complete in time — e.g. a key/value filter with no date bound on a high-volume site. Add or narrow a date range, or reduce pageSize. |
401 | UnauthorizedAccess | Missing or invalid bearer token. Re-run /sw/api/auth or contact support if your token was rotated. |
403 | ForbiddenRequest | Authenticated but not permitted. See structured fields above. |
404 | NotFound | Resource genuinely not found. (Note: /transactions/pdf returns 200 with matches: 0 instead.) |
413 | PayloadTooLarge | Query string exceeded 4,096 characters. Trim filters or split into multiple requests. |
429 | TooManyRequests | Rate limit exceeded. Honor Retry-After. See Rate limits. |
500 | InternalServerError | Server error. Retry; if persistent, contact support@dividia.net. |
Reporting issues
If an API call fails, hangs, or returns unexpected data, email support@dividia.net. To get a faster turnaround, include the request ID and a few details from your side.
The X-Dividia-Request-Id header
Every response from a data endpoint carries an X-Dividia-Request-Id header — an integer identifying the exact row in Dividia's audit log. Capture this in your client logs (alongside the response status and body) every time you make a request.
# Example: capture the header in curl
curl -i "https://api.cloud.dividia.net/sw/api/transactions?serial=2318" \
-H "Authorization: Bearer $TOKEN" | grep -i 'x-dividia-request-id'
# X-Dividia-Request-Id: 84012
When you email support, including this ID lets us look up the exact request without you having to describe the timestamp + URL — we run one query and we're on your specific record.
Endpoints that do not emit the header: POST /sw/api/auth and GET /sw/api/sites. These are pre-data discovery endpoints that aren't logged to the audit table. For failures on those endpoints, include the timestamp (with timezone), the request URL, and the response body in your support email instead.
Support email template
To: support@dividia.net
Subject: API issue: <brief description>
Account: <your account email>
Site serial(s): <e.g. 2318 or 2318,2401>
Request:
<HTTP method> <full URL with query string>
X-Dividia-Request-Id: <value from the response header>
Response:
HTTP <status code>
Body: <copy/paste the JSON response>
Timestamp: <ISO 8601 with timezone, e.g. 2026-06-11T14:35:00-05:00>
Expected: <what you expected to happen>
Observed: <what actually happened>
What NOT to send
- Don't send your API token. The token grants the same access your account has. If you've already sent it, treat it as compromised and email support to request rotation.
- Don't send customer PII (license plates, driver names, etc.) unless it's strictly necessary to explain the issue. If you do, ask support to delete the email thread after resolution.
- Don't paste large response bodies inline. Attach as a file if the body is > ~10KB. The request ID alone is usually enough for us to fetch the body server-side.
Terms of Service
Use of the Dividia ScaleWatcher API is governed by the Dividia API Terms of Service, available at dividia.net/api/terms.
Support
| Channel | Detail |
|---|---|
| support@dividia.net | |
| Phone | +1 (866) 348-4342 |
| Hours | Monday–Friday, 8 AM – 5 PM Central (excluding US federal holidays) |
For API access requests, integration questions, or production issues, email is the fastest path. Include your account email, the site serial(s) involved, the request URL, and the response body if available.
