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. Log in once, store the token, and reuse it for every request: it doesn’t expire (see Token lifetime & rotation). Don’t log in on every poll. Logins are deliberately expensive, and /auth takes only 10 requests per 15 minutes per IP (see Log in once).

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.

PropertyValue
TLSTLS 1.0 and newer accepted; modern clients negotiate TLS 1.2 or 1.3 automatically
HSTSStrict-Transport-Security: max-age=15552000; includeSubDomains
Certificate authorityAmazon 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...

Log in once, then reuse the token

A public-API token doesn’t expire. Call /auth once, store the token, and send it on every request: every poll, every run, every process.

  • Don’t log in on every poll. Logins are deliberately expensive: each one runs a slow password check, and /auth takes 10 requests per 15 minutes per IP. A poller that logs in each time slows down and then gets 429 Too many login attempts, please try again later.
  • Log in again only after a 401 on a token that used to work. That means your API key was rotated (see Token lifetime & rotation).
  • Logging in again doesn’t revoke the tokens you already have. Only a rotation does.
API access enablement. Your Dividia account must have API access turned on and at least one site assigned. If /sw/api/auth returns 403 ForbiddenRequest, contact Dividia (support@dividia.net or 866-348-4342 / 817-288-1040) 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. Log in once with POST /sw/api/auth to get a token for the new key, or wait for the email containing your new token (contact support if it’s 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:

  1. Email support@dividia.net with your origin (scheme + host + port), the integration use case, and how the token will be protected from exposure.
  2. 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. /tickets/pdf has its own per-user limits in place of the per-site ones.

LimitCapWindowScope
Authentication (POST /sw/api/auth)10 requests15 minper IP
General API (authenticated)1,000 requests15 minper IP
Per-(user, site)60 requests1 minper (user, site) — each site in a multi-site request counts independently
Per-site (across all users)60 requests1 minper site — each site in a multi-site request counts independently
Duplicate-request guardidentical params, same endpoint10 secper (user, site) — an identical request to the same endpoint that arrives while an earlier one is still running gets 429 TooManyRequests with message “Duplicate request”; the earlier one is served (not on /tickets/pdf, which has the one-at-a-time rule below)
Tickets PDF (GET /sw/api/tickets/pdf)60 requests1 minper API user, for every call to it (all your sites or only the ones you name), in place of the per-(user, site) and per-site limits; its calls don’t count toward those
Tickets PDF, one at a time1 request running—per API user — a call while another is still running gets 429 with Retry-After: 60 (a call that never finished stops blocking after 10 minutes)

Response headers

Successful responses and rate-limit 429s carry RFC-draft rate-limit headers describing your current budget:

HeaderMeaning
RateLimit-LimitThe applicable cap for this request.
RateLimit-RemainingApproximate number of requests remaining in the current window.
RateLimit-ResetSeconds until the window resets.
Retry-AfterOn 429: seconds to wait before retrying.

Headers use the unprefixed RateLimit-* naming from the IETF RFC draft — not the legacy X-RateLimit-* form.

Which limit do the headers describe? On data and PDF endpoints (everything except /auth and /sites), RateLimit-* means one thing: the binding per-site cap (60 requests / minute), or on /tickets/pdf your per-user budget (also 60 / minute). A 200 carries it, so RateLimit-Remaining counts down toward the limit that actually throttles you and you can self-pace against it; a per-site or per-user 429 carries it with RateLimit-Remaining: 0. A response that ends before that budget is checked carries no RateLimit-* headers: for example a 403, a 413, the duplicate-request 429 and the /tickets/pdf one-at-a-time 429. (A 401, or a 400 for a missing or malformed serial or locationId, comes from an earlier layer and can carry the general per-IP headers; don’t pace on those.) /auth and /sites have no per-site limiter: on /auth the headers describe the login cap (10 / 15 min per IP), and on /sites the global per-IP cap (1,000 / 15 min).

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 for your account on site serial 4001"
  }
}

Per-(user, site) and per-site 429s carry all four headers above, as does the /tickets/pdf per-user 429. 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. Of two identical requests to the same endpoint that arrive together, the first is served and the later one gets this 429. The same parameters sent to two different endpoints aren’t duplicates. The /tickets/pdf one-at-a-time 429 also carries only Retry-After (60).

Multi-site request accounting

A request for several sites (for example ?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.

Buckets are kept per site, after your serial and locationId values are resolved to sites (see Choosing sites). A site counts in the same bucket however you name it: by its serial written another way (02318 for 2318), or by its location id.

/tickets/pdf is never checked against the per-site buckets, and its calls don’t count toward them. Every call to it, for all your sites or only named ones, counts once against your per-user /tickets/pdf budget, and never against a site’s per-(user, site) or per-site limits on the other endpoints.

Which requests count

A request that’s turned away doesn’t count toward the per-(user, site), per-site or /tickets/pdf limits: a 403, a 413, or a 400 BadRequest for a bad parameter. Everything else counts. That includes a 400 sent after the query has run (PageOutOfRange for a page past the last one, or QueryTooBroad), and a 429: retrying before Retry-After keeps you over the limit.

Higher limits

Higher limits are available on request — contact support@dividia.net with your expected request volume and use case.


Conventions

Choosing sites: serial and locationId

Every data and PDF endpoint (everything except /auth and /sites) picks its sites the same way. Name them with either parameter, or both:

  • serial: site serials, one or a comma-list (serial=2318,2401,4001). It may contain only digits, spaces and commas; anything else is a 400: serial must be a positive integer or comma-list of positive integers.
  • locationId: Command Cloud location ids, one or a comma-list (locationId=PLANT-7,LOC-12), each up to 64 Latin-1 characters. Each one is matched exactly (case- and punctuation-sensitive; spaces around it are ignored) against each site’s own locationId, the one /sites shows. A location id picks every site on your key that uses it. A site with no parent customer doesn’t count as a separate customer, so a stand-alone legacy site and the cloud site that replaces it are both returned. If a location id you send matches sites under two or more different customers (possible when your key has sites at several customers), the request is a 400: location id(s) PLANT-7 match sites at more than one customer; use serial instead. Name those sites by serial.

Repeating a parameter is the same as a comma-list: locationId=PLANT-7&locationId=LOC-12 means locationId=PLANT-7,LOC-12, and serial works the same way.

The sites are combined and de-duplicated, up to 10 per call. More than 10 entries in either list, or more than 10 sites in all, is a 400 (the messages are under Common errors). Every endpoint needs at least one site, except /tickets/pdf, which defaults to all your sites. The sites you can use come back in the /auth and /sites responses.

A multi-site result is one combined list. Pagination spans it in the endpoint’s order (see Pagination & ordering), and each row carries its own serial so you can split it client-side.

locationId picks sites; it doesn’t filter rows. To narrow rows to one of your Command Cloud scales, use scaleId (see Command Cloud identifiers).

403 for a site you can’t use. If any site you name, by serial or by location id, doesn’t exist or isn’t in your access, the whole request fails with the same 403, on every endpoint. A deleted site counts as one that doesn’t exist. All of these get the same answer, so a reply never reveals whether a site exists. The response lists what it refused, serials in unauthorizedSerials and location ids in unauthorizedLocationIds (each only when it has entries):
{
  "error": {
    "code": 403,
    "type": "ForbiddenRequest",
    "message": "Access denied for site serial(s): 9999, 8888; location id(s): PLANT-9",
    "unauthorizedSerials": [9999, 8888],
    "unauthorizedLocationIds": ["PLANT-9"]
  }
}
The arrays are machine-parseable; the message field is human-readable. It names only what it refused: Access denied for site serial(s): 9999, Access denied for location id(s): PLANT-9, or both joined by ; as above. If you typo’d a serial or location id, or you’re not in the access list for a site you expected to be, this tells you which.

Two other 403s: Site(s) without API access: 2318 (with apiDisabledSerials) for a site on your list whose API access is switched off, and User has no API access for any site when your key has no sites at all. A site with API access off that you name by location id gets the Access denied answer instead.

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. The facilityDuration* aggregates on /transactions/metrics are zero for these sites; scaleDuration* and waitDuration* may carry values from scale-event timestamps.
  • lpn — truck plus license-plate-recognition events. LprRead events appear in /events and populate the transaction lpn field. Same facility-duration caveat as truck.
  • 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.

Command Cloud identifiers (scaleId, locationId) new

If your tickets reach Dividia through a Command Cloud webhook integration, every transaction, event, and alert carries the two identifiers you sent — alongside Dividia’s own serial and scale — so you can read and filter your data entirely in your own terms:

FieldTypeWhat it is
scaleIdstringYour Command Cloud scale identifier, stored and matched byte-for-byte (case- and punctuation-sensitive — e.g. IQ-001:A). Dividia maps it to a physical scale lane internally.
locationIdstringYour Command Cloud location.id for the site — maps to Dividia’s serial; one location id can pick more than one of your sites (see Choosing sites). Also exposed per-site on /sites.

Both are null on records that did not originate from Command Cloud. In a request they do different jobs:

Text values are Latin-1

On every data and PDF endpoint, each parameter value must use only Latin-1 (ISO 8859-1) characters: plain ASCII plus the accented letters and symbols of Western European languages. A value with any other character, such as an emoji, curly quotes (“ ”), or Cyrillic or Chinese text, can’t match anything Dividia stores, so it’s rejected before the query runs, with 400 BadRequest and a message that names the parameter:

Invalid 'customer' value: only Latin-1 (ISO 8859-1) characters are supported

The one exception is scaleId, which takes any text, because your Command Cloud scale ids are stored exactly as you sent them. (A locationId also can’t contain control characters.)

Pagination & ordering

All list endpoints accept these common query parameters:

ParameterTypeDefaultDescription
pageinteger11-based page number. An omitted or empty value uses page 1. Values less than 1 or non-integers return 400 BadRequest.
pageSizeinteger100Items 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. The PDF endpoints take limit (1–50) instead, and no pageSize, orderBy or orderDir.
orderBystringtimestamp; on /events, transactionIdField to sort by: one of that endpoint’s filter parameters, by its filter-param name, not its response-field name (orderBy=startDate sorts /transactions by startTime). orderBy=locationId also works on /transactions, /events and /alerts: it sorts by each row’s locationId, even though the locationId parameter picks sites rather than filtering rows. Any other value is ignored and the default order applies, with no error.
orderDirstringASCDESC (any case) sorts in descending order; any other value sorts ascending.
Query string limit: 4,096 characters in total, counted as the JSON of the decoded parameter names and values (slightly longer than the query string itself). Over that → 413 PayloadTooLarge, on every data endpoint except /tickets/pdf, whose parameters are all short. 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, metrics and discovery queries (/transactions, /transactions/metrics, /events, /events/types, /events/keys, /alerts) are always bounded by time:

  • No date filter → last 90 days. If you supply none of date, startDate, endDate, or since (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 an X-Dividia-Default-Window: 90d header, so it’s never a silent surprise. To reach further back, pass an explicit range. /transactions/metrics doesn’t support since, so there it doesn’t change the window.
  • Record lookups are exempt. A query that pins a specific record returns it regardless of age, with no default window: by id; by ticket on /transactions and /events; or by transactionId on /events and /alerts. /transactions/metrics, /events/types and /events/keys always use a window.
  • Maximum span: 366 days. An explicit startDate/endDate range — or a since reaching further back than that — wider than 366 days is rejected with 400 DateRangeTooLarge, for example Requested date range (400 days) exceeds the maximum of 366 days. Split the request into smaller date ranges.

The PDF endpoints work differently. /transactions/pdf has no default window and no maximum span, but needs at least one filter; /tickets/pdf pages through your tickets with a bookmark.

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. Predicate fields read as positive assertions rather than negative ones (complete: true, never hidden: true). Two types don’t match their name’s first impression: ticket is a string (e.g. "9999", not a number), and the alert flagged field comes back as 1/0 (truthy integer, not true/false) — the per-endpoint response shapes below are authoritative for exact types. 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 or unauthorizedLocationIds, 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, /tickets/pdf, /events/types, /events/keys) — success or error — also carries an X-Dividia-Request-Id response header. The exceptions are a 401 and the 400 for a missing or malformed serial or locationId (Site serial number or locationId is required, Invalid site serial, Invalid locationId): those are answered before the request is logged. 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 data endpoints, all reads (GET), plus one POST endpoint: /auth.

MethodPathPurpose
POST/sw/api/authExchange creds for a JWT
GET/sw/api/sitesList sites your account can access
GET/sw/api/transactionsList ticket transactions with filters + pass-through kv
GET/sw/api/transactions/metricsAggregate stats with dimension or time-bucket groupBy
GET/sw/api/transactions/pdfTransaction PDFs by filter → one PDF, a ZIP, or 0-match JSON
GET/sw/api/tickets/pdfTicket PDFs for your records: a bookmark feed of new tickets, or one ticket by number new
GET/sw/api/eventsList individual events with filters + pass-through kv
GET/sw/api/events/typesDistinct event-type names seen at the site (recent window)
GET/sw/api/events/keysDistinct JSON field names seen in event payloads (recent window)
GET/sw/api/alertsList 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. Log in once and reuse the token: it doesn’t expire, and logins are deliberately expensive, so logging in on every call or every poll is slow and soon exhausts the auth rate limit. See Log in once.

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", "locationId": "PLANT-7" },
      { "id": 43, "name": "Site B", "serial": 2212, "parentName": "Parent Co", "customerType": "truck", "locationId": null },
      { "id": 44, "name": "Site C", "serial": 2401, "parentName": "Parent Co", "customerType": "facility", "locationId": null }
    ]
  }
}

Errors

StatusMessage
400Email and password are required
401Invalid email or password. Please check your credentials and try again.
403Your account was found, but API access has not been enabled. Please contact Dividia to request API access.
403Your account has API access enabled, but no sites have been assigned. Please contact Dividia to configure your site access.
429Too many login attempts, please try again later. (the 10 per 15 minutes per IP limit)

Both 403s also carry a contact object: {"email": "support@dividia.net", "phone": "866-348-4342"}.

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", "locationId": "PLANT-7" },
    ...
  ]
}

The customerType field is always one of facility, truck, or lpn. See Site customer types for what each value means.

The locationId field new is the site’s Command Cloud location.id — present (a string) for sites whose tickets arrive via Command Cloud, and null otherwise. It’s the bridge from your Command Cloud location to Dividia’s serial: the data and PDF endpoints accept it in place of serial (see Choosing sites). See also Command Cloud identifiers.

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

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
idintegeroptionalInternal transaction id.
scaleintegeroptionalScale index within the site (the physical lane number).
scaleId newstringoptionalCommand Cloud scale identifier. Exact-match, byte-for-byte (case- and punctuation-sensitive). See Command Cloud identifiers.
lpnstringoptionalLicense plate. Best-effort: only populated when an LprRead event landed on the transaction.
truckstringoptionalTruck id (from the site’s ticketing integration).
ticketintegeroptionalLoadout ticket number. The filter takes a numeric value, but the field is stored and returned as a string (e.g. "9999") — don’t assume it’s numeric in responses.
datedateoptionalExact day, YYYY-MM-DD.
startDatedateoptionalInclusive lower bound, YYYY-MM-DD.
endDatedateoptionalInclusive upper bound, YYYY-MM-DD.
sinceISO 8601 datetimeoptionalExclusive 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.
includeEventsbooleanoptional1 to nest per-transaction events. Any non-empty value turns it on, 0 included, so leave the parameter out to skip them.
includeAlertsbooleanoptional1 to nest per-transaction alerts. Like includeEvents, any non-empty value turns it on.
any other keystringoptionalPass-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,
  "scaleId": "IQ-001:A",
  "locationId": "PLANT-7",
  "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": -5
}
The 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).
The 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).
Pending (imageless) Command Cloud tickets new. A ticket that arrives via a Command Cloud webhook is recorded immediately — before the on-site NVR uploads its recorded weighment. Until that upload lands, the transaction is pending and carries extra fields:
"imageless": true,
"status": "pending",
"complete": false,
"reason": "We received this ticket from Command Cloud, but your NVR has not uploaded the recorded weighment yet. Dividia is looking into it; the ticket will appear in your transactions once the recording is uploaded."
These fields appear only on pending rows — an ordinary captured transaction omits imageless/status/reason. Note complete is false on a pending row even for truck/lpn sites that are otherwise always complete: true (a pending ticket isn’t a finished weighment). A pending row is superseded automatically once the NVR uploads — so a ticket may show as pending briefly, then resolve to a normal transaction; one whose NVR never uploads stays pending. See Command Cloud identifiers.

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"

# Two Command Cloud locations, named by your own location ids
curl "https://api.cloud.dividia.net/sw/api/transactions?locationId=PLANT-7,LOC-12&date=2026-06-10" \
  -H "Authorization: Bearer $TOKEN"

GET/sw/api/transactions/metrics

Aggregate counts and duration stats for transactions matching the supplied filters. Accepts most of the /sw/api/transactions filters (including pass-through kv; the list is below) and adds the groupBy parameter to slice by time bucket or dimension.

Parameters

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
groupByenumoptionalOne of daily, hourly, carrier, customer, product, scale. Omit for a single summary row.
Plus these filters from /sw/api/transactions: id, scale, lpn, truck, ticket, date, startDate, endDate, and pass-through kv. Not scaleId, since, includeEvents or includeAlerts: like any other unrecognized name, they’re read here as pass-through event-data filters, so they don’t do what they do on /transactions. A since here also doesn’t switch off the default 90-day window. Filter /transactions directly if you need to slice by your Command Cloud scale id (see Command Cloud identifiers), and use date or startDate/endDate for time.

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 transactions as PDFs, for targeted or larger searches: by transaction id, ticket number or range, date, Command Cloud scaleId or event data. One id or one ticket returns a single PDF; everything else returns a ZIP of PDFs with a manifest.json. To collect every new ticket for your own records, use GET /sw/api/tickets/pdf instead.

Parameters

Name at least one site, with serial and/or locationId (Choosing sites), and at least one filter. Filters combine with AND.

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
idinteger or comma-listoptionalOne transaction id, or up to 50 comma-listed: id=12345 or id=12345,12346,12347.
ticketstring or comma-listoptionalOne ticket number, or up to 50 comma-listed. Any ticket text of up to 30 characters (ticket=37237908, ticket=AB-77), matched exactly.
ticketMin, ticketMaxintegeroptionalNumeric ticket range, inclusive. ticketMin works on its own (every ticket from that number up); ticketMax needs ticketMin. Tickets that aren’t numbers fall outside any range.
datedate (YYYY-MM-DD)optionalOne day in the site’s local time, matched against each transaction’s endTime.
startDate, endDatedate (YYYY-MM-DD)optionalInclusive range of site-local days. Send both.
scaleId newstringoptionalCommand Cloud scale identifier, exact match (byte-for-byte).
data.<key> newstringoptionalEvent-data filter (pass-through kv), written with a data. prefix: data.customer=ACME. Up to 8 per request.
pageintegeroptional1-based page. Default 1. Read leniently: 0, a negative number or a non-number reads as 1, never a 400.
limitintegeroptionalPDFs per page, 1–50. Default 25.
excludeImagesbooleanoptional1 or true leaves the images out, for a much smaller download. 0 or false (the default) keeps them.

Dates must be real calendar days written YYYY-MM-DD: a datetime, or a day like 2026-02-30, is a 400. The endpoint takes no other parameters: pageSize, since, a typo like tickets, or an event-data key without the data. prefix (customer=ACME, which this endpoint used to accept) returns 400, and the message lists the accepted parameters. Deleted transactions and deleted events are never included.

Responses

RequestStatusContent-TypeBody
One id or one ticket, matching one transaction200application/pdfThe PDF.
Everything else, even when only one transaction matches200application/zipOne PDF per transaction on the page, plus manifest.json. One ticket that matches several transactions (the same number at two sites, or uploaded twice) is a ZIP too, so none is dropped.
No matches200application/json{"matches": 0, ...}, shown below.
A page past the last one400application/jsonThe error envelope, type PageOutOfRange, shown below.
A search too broad to finish within the time limit400application/jsonThe error envelope, type QueryTooBroad, message Query exceeded the time limit. Narrow the date range (use date=YYYY-MM-DD for a single day, or a smaller startDate/endDate span), pick fewer sites with serial or locationId, or remove broad data.<key> filters, then retry. (see Error code reference).

A page past the last one:

{
  "error": {
    "code": 400,
    "type": "PageOutOfRange",
    "message": "page 3 of 2 does not exist (51 matches at limit 50)"
  }
}

Every non-error response carries paging headers: X-Total-Count (matching transactions), X-Page, X-Pages and X-Limit. Transactions come in a fixed order: newest endTime first, ties by transaction id, highest first.

Long transactions and slow downloads. A PDF shows at most 1,000 events per transaction, the first 1,000 in time order. A longer transaction’s event list ends with N more events not shown. A ZIP download stops after 10 minutes, counted from when the request started. The ZIP is then incomplete and can’t be opened: request the page again, with a smaller limit or excludeImages=1 if your connection is slow. At 200 KB/s, the largest ZIP (50 PDFs with images, about 63 MB) takes about 5.5 minutes.

File names

  • <serial>-ticket_<ticket>.pdf, for example 2318-ticket_37237908.pdf.
  • A Command Cloud ticket adds its locationId: <serial>-<locationId>-ticket_<ticket>.pdf, for example 10035-LOC-12-ticket_37237908.pdf.
  • No ticket number (blank or 0): <serial>-transaction_<id>.pdf.
  • Ticket text and location ids keep only letters, digits and -; any other character becomes -.
  • A site with more than one transaction for the same ticket number (the same weighment uploaded twice): the lowest transaction id gets the plain name, and every other one is named <name>_<transactionId>.pdf, for example 4101-ticket_1001_313.pdf. A transaction gets the same name on every page, at every limit and on both PDF endpoints, so saving by file name never overwrites a different transaction.
  • To read a ticket number from a name, take the text after the last ticket_, up to the _<transactionId> that any but the lowest-id transaction for that ticket carries. Ticket text never contains _, so the rule always holds.
  • The ZIP itself is <serial>-transactions_<timestamp>.zip, or multi_<N>sites-transactions_<timestamp>.zip when the request covers several sites.

File names are for people. To tie a PDF to your data, read manifest.json rather than parsing names.

manifest.json

{
  "files": [
    { "serial": 10035, "locationId": "LOC-12", "ticket": "37237908", "transactionId": 5512,
      "file": "10035-LOC-12-ticket_37237908.pdf" },
    { "serial": 10035, "locationId": null, "ticket": null, "transactionId": 5513,
      "file": "10035-transaction_5513.pdf" }
  ],
  "failed": []
}

One entry per PDF, in ZIP order. ticket is the ticket number as stored (null when there isn’t one), and file is the entry’s name in the ZIP. failed lists, with the same fields, any transaction whose PDF couldn’t be built; request those again later.

Zero-match response

{
  "matches": 0,
  "filter": {
    "serial": 2318,
    "startDate": "2026-06-01",
    "endDate": "2026-06-07",
    "data.carrier": "ACME",
    "data.dispatcher": "jane"
  },
  "keyNeverSeen": ["data.dispatcher"],
  "valuesDidNotMatch": ["data.carrier"],
  "message": "No transactions matched the supplied filters. data.dispatcher has never appeared in any event for this site — your integration may store it under a different key name. data.carrier has been seen but no event matched the supplied value."
}

keyNeverSeen and valuesDidNotMatch tell a key name that never appears at these sites apart from a value that didn’t match. Both are empty when you sent no data. filters. This hint is best-effort: when working it out would take too long, it’s left out of the response.

filter echoes the search as it ran: serial is the site searched (an array when there are several, including the sites a locationId picked), id comes back as ids (an array), a comma-list of tickets as tickets (one ticket stays ticket), and each event-data filter under its data.<key> name.

400 errors

  • No filter (only sites), or an unknown parameter (the message lists the accepted ones).
  • A data. key that isn’t a valid key name, or data.<key> or scaleId sent twice.
  • More than 8 event-data filters: Too many data.<key> filters: at most 8 per request.
  • More than 50 values in id or ticket; an id that isn’t a positive whole number; an empty ticket, or one longer than 30 characters.
  • ticketMax without ticketMin; a ticketMin or ticketMax that isn’t a positive number; ticketMin greater than ticketMax.
  • A date, startDate or endDate that isn’t a real YYYY-MM-DD day; only one of startDate and endDate; startDate after endDate.
  • A limit that isn’t a whole number from 1 to 50; an excludeImages other than 1, true, 0 or false; a page past the last one.
  • No site (neither serial nor locationId), a malformed one, more than 10 sites, or a location id that matches sites at more than one customer (the messages are under Common errors).
  • A value with a character outside Latin-1, in any parameter but scaleId (see Text values are Latin-1).

Examples

# One ticket → a single PDF
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticket=37237908" \
  -H "Authorization: Bearer $TOKEN"

# Several tickets → ZIP
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticket=37237908,37237909,AB-77" \
  -H "Authorization: Bearer $TOKEN"

# Several transaction ids → ZIP
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&id=12345,12346,12347" \
  -H "Authorization: Bearer $TOKEN"

# A week at one Command Cloud location, named by its location id, filtered on event data
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?locationId=LOC-12&startDate=2026-06-01&endDate=2026-06-07&data.customer=ACME" \
  -H "Authorization: Bearer $TOKEN"

# Every ticket from 37237900 up, without images
curl -OJ "https://api.cloud.dividia.net/sw/api/transactions/pdf?serial=2318&ticketMin=37237900&excludeImages=1" \
  -H "Authorization: Bearer $TOKEN"

GET/sw/api/tickets/pdf new

Ticket PDFs for your own records, for example a copy on your own servers. Pull on your own schedule, every 15 minutes or longer. Each call returns tickets you don’t have yet, up to 50 at a time, across every site your key can see or only the ones you name. Two modes:

  • The feed (the default): new tickets in the order Dividia received them, with a bookmark, next, that you send back on your next call.
  • One ticket: ticket=<number> fetches a single ticket by its number.

Only transactions with a real ticket number are included (not blank or 0). For searches by date, id or event data, use /transactions/pdf.

Parameters

NameTypeRequiredDescription
serial, locationIdcomma-listsoptionalThe sites to pull: serials, Command Cloud location ids, or both, up to 10 sites (see Choosing sites). Without either, every site your key can see (/sites). The usual 403 rules apply. For one ticket, send exactly one serial or one locationId, not both.
scaleIdstringoptionalCommand Cloud scale identifier, exact match.
afterstringoptionalFeed: the next value from your last manifest.json, sent back unchanged.
fromdate (YYYY-MM-DD)optionalFeed, first call only: start with the tickets Dividia received on or after that day (from midnight UTC). With neither after nor from, the feed starts at your oldest ticket.
limitintegeroptionalFeed only: tickets per call, 1–50. Default 50.
ticketstringoptionalOne ticket: its number, as text of up to 30 characters, with exactly one serial or one locationId, not both. Ticket numbers are only unique per site.

Send each parameter once, except serial and locationId: repeating one of those is the same as a comma-list. Any other parameter, including page or excludeImages, returns 400 with a message listing the accepted ones. So does a bad value or combination (the messages are below): after and from together; a from day after today or an after bookmark later than now (UTC); ticket with after, from or limit; or ticket without exactly one serial or one locationId. PDFs from this endpoint always include their images.

400 errors

MessageWhen
Unknown parameter(s): page. Accepted parameters: serial, locationId, scaleId, after, from, limit, ticket.Any other parameter.
limit must be supplied onceA parameter other than serial or locationId sent twice (the message names it).
Invalid 'ticket' value: only Latin-1 (ISO 8859-1) characters are supportedA value with a character outside Latin-1, in any parameter but scaleId (the message names it; see Text values are Latin-1).
serial comma-list exceeds max 10 entries and the other site messagesA malformed serial or locationId, more than 10 sites, or a location id that matches sites at more than one customer (see Choosing sites).
limit must be a whole number from 1 to 50Feed: a limit outside 1–50.
Send after or from, not both: from only starts a new feedFeed: after and from together.
Invalid after. Send the next value from the last manifest.json unchangedFeed: a bookmark Dividia didn’t issue.
Invalid after. The bookmark is later than now (UTC); send the next value from the last manifest.json unchangedFeed: a bookmark from the future.
Invalid from. Use a real calendar date as YYYY-MM-DDFeed: a from that isn’t a real day.
Invalid from. It is later than today (UTC)Feed: a from day after today (UTC). Today is fine.
ticket fetches one ticket and cannot be combined with after or fromOne ticket: ticket with after or from.
a ticket lookup needs exactly one serial or one locationId (ticket numbers are only unique per site)One ticket: no site, several, or both a serial and a locationId.
limit doesn't apply to a ticket lookupOne ticket: ticket with limit.
Invalid ticket parameter (expected ticket text of up to 30 characters)One ticket: an empty ticket, or one longer than 30 characters.

A call too broad to finish within the time limit gets 400 QueryTooBroad (see Error code reference), message Query exceeded the time limit. Pick fewer sites with serial or locationId, or send a later from or after, then retry.

The feed

Each call returns a ZIP of the next tickets, oldest first by when Dividia received them, plus manifest.json. It is a ZIP even when nothing is new: then files is empty, hasMore is false, and next is the bookmark you sent (on a first call, a bookmark for where you started).

A ticket becomes available 2 hours after Dividia receives it. By then a Command Cloud ticket usually has its recorded weighment and images, and Dividia’s automatic matching (such as joining a facility entry and exit) is done, so most tickets arrive once, complete. To get one sooner, use ticket=, which has no delay.

manifest.json

{
  "next": "MjAyNi0wOS0yOCAxMjowNTowNy4xMjMwMDAsOTk4NzY1",
  "hasMore": false,
  "files": [
    {
      "serial": 10035,
      "locationId": "LOC-12",
      "ticket": "37237908",
      "transactionId": 998765,
      "siteName": "Acme Aggregates - Plant 12",
      "scaleId": "IQ-001:A",
      "weighedAt": "2026-09-28T07:04:51-05:00",
      "receivedAt": "2026-09-28T12:05:07.123Z",
      "status": "complete",
      "file": "10035-LOC-12-ticket_37237908.pdf"
    }
  ],
  "failed": []
}
FieldWhat it is
nextThe bookmark for your next call (after=). Also sent in the X-Next-After response header.
hasMoretrue when more tickets are ready now: call again right away.
filesOne entry per PDF in the ZIP, in feed order.
failedTickets whose PDF couldn’t be built, with the same fields. The feed moves past them, so fetch them later with ticket=.

Each entry in files:

FieldTypeWhat it is
serialintegerSite serial.
locationIdstring or nullCommand Cloud location.id; null for other tickets.
ticketstringThe ticket number, as stored.
transactionIdintegerDividia’s transaction id (the id in /transactions).
siteNamestringThe site’s name, as printed on the PDF.
scaleIdstring or nullCommand Cloud scale identifier; null for other tickets.
weighedAtISO 8601When the truck was weighed, in the site’s local time with its offset (the transaction’s endTime).
receivedAtISO 8601, UTCWhen this transaction got its ticket at Dividia: when we received it, or when it was last moved onto another transaction (see below). The feed runs in this order.
statusstringcomplete, or pending: a Command Cloud ticket whose site hasn’t uploaded the recorded weighment yet, so the PDF has the ticket data but no images. See pending (imageless) tickets.
filestringThe PDF’s name in this ZIP, named like /transactions/pdf files: <serial>[-<locationId>]-ticket_<ticket>.pdf, or <name>_<transactionId>.pdf when the site has another transaction for the same ticket with a lower id. A transaction always gets the same name, in every call and on both PDF endpoints.

The bookmark loop

  1. First run: call with from=YYYY-MM-DD, or with nothing to start from your oldest ticket. Every later call: after=<your saved bookmark>.
  2. Unzip and write every PDF. A PDF whose name you already have replaces the old file (see below for when that happens).
  3. Save next only after the files are written. If a run fails before that, the next run repeats the batch and nothing is lost.
  4. If hasMore is true, call again right away. Otherwise you’re caught up until your next run.
  5. If failed isn’t empty, fetch those tickets later with ticket=.

Treat next as an opaque string and send it back exactly as you received it; a bookmark Dividia didn’t issue returns 400. A bookmark is a position, not a scope, so keep a separate one for each scope (serial, locationId, scaleId) you pull. If you lose your bookmark, start again with from=: files you already have are simply overwritten.

A site newly added to your API access: its older tickets aren’t in a feed you already have running, because they sit behind your bookmark. Pull them once with serial=<site> (or locationId=) and from=<date>, calling again while hasMore is true. Your running feed carries on as before.

# First run
curl -o tickets.zip "https://api.cloud.dividia.net/sw/api/tickets/pdf?from=2026-09-01" \
  -H "Authorization: Bearer $TOKEN"

# Every later call: after = the next value from the last manifest.json
curl -o tickets.zip "https://api.cloud.dividia.net/sw/api/tickets/pdf?after=MjAyNi0wOS0yOCAxMjowNTowNy4xMjMwMDAsOTk4NzY1" \
  -H "Authorization: Bearer $TOKEN"

# One Command Cloud location only (with its own bookmark)
curl -o tickets.zip "https://api.cloud.dividia.net/sw/api/tickets/pdf?locationId=LOC-12&from=2026-09-01" \
  -H "Authorization: Bearer $TOKEN"

One ticket

ticket=<number>, with exactly one serial or one locationId, not both (ticket numbers are only unique per site), and optionally scaleId, fetches that ticket right away, with no 2-hour delay. One match returns the PDF (application/pdf). Several matches (the same ticket uploaded twice at the site) return a ZIP whose manifest.json has files and failed but no next or hasMore. No match returns 200 JSON:

{
  "matches": 0,
  "filter": { "ticket": "37237908", "serial": 10035, "locationId": "LOC-12" },
  "message": "No ticket matched the supplied filters."
}

filter always has serial, the site searched, plus the locationId and scaleId when you sent them.

curl -OJ "https://api.cloud.dividia.net/sw/api/tickets/pdf?serial=2318&ticket=37237908" \
  -H "Authorization: Bearer $TOKEN"

Limits

  • One request at a time per API user. A call while another of yours is still running returns 429 Another /tickets/pdf request for this account is still running with Retry-After: 60. Run one puller per API user. (A call that never finished stops blocking after 10 minutes.) These limits are per API user, that is per key: “account” in the messages means the API user, and two API users with access to the same sites each get their own.
  • 60 requests per minute per API user, shown in the RateLimit-* headers. Past that, 429 Too many /tickets/pdf requests for your account with Retry-After: 60. This budget covers every call, the whole-account feed included, and replaces the per-site limits, which aren’t checked on this endpoint. Calls here don’t count toward the per-site limits on the other endpoints either, even when they name sites (see Multi-site request accounting).
  • Run every 15 minutes or longer. Within a run, keep calling while hasMore is true.
  • A ZIP download stops after 10 minutes, counted from when the request started, the same 10 minutes as the one-at-a-time rule. The ZIP is then incomplete, with no manifest.json, and can’t be opened. Don’t save its next (the X-Next-After header included): call again with the same after, and nothing is skipped. At 200 KB/s the largest ZIP (50 PDFs, about 63 MB) takes about 5.5 minutes.
  • At most 1,000 events per transaction in a PDF, the first 1,000 in time order. A longer transaction’s event list ends with N more events not shown.

When your counts can differ from ours

Count unique site and ticket pairs (serial and ticket in manifest.json), not file names, and group them by weighedAt. A count kept that way can still differ from ScaleWatcher’s in these cases.

Sent twice. A ticket sent again keeps its file name, so the new copy replaces the older file when you write it. No two current transactions share a file name, so saving by name never overwrites a different transaction.

  1. A Command Cloud ticket whose site uploads the recorded weighment more than 2 hours after we receive the ticket. It comes first with status: "pending" and no images, then again with status: "complete", under the same file name.
  2. A ticket merged into another transaction after it was sent, either by Dividia staff or by a late automatic match (for example, joining a facility entry and exit). It comes again with the added events, under the same file name.
  3. A weighment the site uploaded twice: two transactions with the same site and ticket number. The lower transaction id gets the plain file name and the other one gets <name>_<transactionId>.pdf (for example 4101-ticket_1001_313.pdf), in every call, so you keep both files. That’s why you count site and ticket pairs from manifest.json, not files.

Never sent.

  1. Tickets we haven’t received. A site whose NVR is offline sends nothing until it’s back online and uploads; those tickets then arrive late, in the order we receive them. Command Cloud tickets reach us through the webhook, so we have their ticket data even while the site is offline.
  2. Tickets deleted, or whose ticket number is removed, within 2 hours of arriving.
  3. Transactions without a real ticket number (blank or 0).

Changed on our side after sending. Your copy stays as it was; we don’t send corrections or deletions.

  1. A ticket deleted, or taken off its transaction, after it was sent. If a transaction ends up with a different ticket number, that number arrives as a new file, and the old file stays.

Content and timing.

  1. A PDF can be missing an image if the image failed to save when the site uploaded it.
  2. weighedAt comes from the site NVR’s clock. A few sites’ clocks are off by hours, which can move a ticket to the wrong day in a daily count.
  3. Tickets arrive in the order we receive them, not the order they were weighed. Count by weighedAt, not by when you pulled them.

On your side.

  1. Save next only after the files are written. If the bookmark is lost, start again with from=; files you already have are simply overwritten.
  2. When a site is newly added to your API access, its older tickets aren’t in a feed you already have running. Pull them once with serial=<site> (or locationId=) and from=<date>.

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

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
idintegeroptionalEvent id.
transactionIdintegeroptionalFilter to one transaction.
namestringoptionalEvent name (e.g. LprRead, TicketReceived). Use /events/types to discover valid values.
ticketintegeroptionalLoadout ticket number. Filter takes a numeric value; returned as a string.
scaleId newstringoptionalCommand Cloud scale identifier (byte-exact). Carried from the parent transaction. See Command Cloud identifiers.
date / startDate / endDatedateoptionalDate filtering.
sinceISO 8601 datetimeoptionalExclusive lower bound on timestamp. For incremental polling; see Polling patterns.
any other keystringoptionalPass-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",
  "scaleId": "IQ-001:A",
  "locationId": "PLANT-7"
}

The images array is pre-signed for direct fetch — see Image URLs.

An event on a pending Command Cloud ticket also carries "status": "pending" and the same reason as its pending transaction.

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

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
categoryenumoptionalalert returns only event types whose name starts with Alert (AlertOverweight, AlertNoTicket, etc.). non-alert returns the complement. Omit for all event types. Any other value is a 400.
date / startDate+endDate / sincedate / datetimeoptionalRestrict 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"

A successful response carries Cache-Control: private, max-age=3600, so your client may cache it for an hour. Parameters this endpoint doesn’t use are ignored.


GET/sw/api/events/keys

Distinct JSON field names seen at the top level of event data or one level under data.ticket (where TicketReceived keeps its payload) within the query window. No key named ticket is listed; at the top level that’s the container. 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

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
eventNamestringoptionalNarrow 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.
categoryenumoptionalalert narrows to keys seen in any Alert-prefixed event. non-alert is the complement. Composes with eventName via AND (intersection).
date / startDate+endDate / sincedate / datetimeoptionalRestrict 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": 6,
  "data": [
    { "key": "lpn",        "seenIn": ["LprRead"],         "events": 84012 },
    { "key": "confidence", "seenIn": ["LprRead"],         "events": 84012 },
    { "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). A successful response carries Cache-Control: private, max-age=3600, so your client may cache it for an hour. Parameters this endpoint doesn’t use are ignored.


GET/sw/api/alerts

List alerts (overweight, no-ticket, etc.) raised against transactions on this site.

Parameters

NameTypeRequiredDescription
serial, locationId newcomma-listsrequiredThe sites to query: serials, Command Cloud location ids, or both, up to 10 sites. Send at least one. See Choosing sites.
idintegeroptionalAlert id.
namestringoptionalAlert type (e.g. AlertOverweight, AlertNoTicket).
transactionIdintegeroptionalFilter to one transaction.
statusstringoptionalAlert status (open, acknowledged, etc.).
flaggedbooleanoptional1 for flagged-only.
priorityenumoptionalFilter value is lowercase: low, medium, or high. Note the response field comes back capitalized — "High"/"Medium"/"Low".
scaleintegeroptionalScale index (physical lane number) on the alert’s transaction.
scaleId newstringoptionalCommand Cloud scale identifier (byte-exact). Carried from the parent transaction. See Command Cloud identifiers.
hasCommentbooleanoptional1 to limit to commented alerts.
commentstringoptionalSubstring search across comment text.
assignedUserId / assigneeinteger / stringoptionalFilter by assignment: the user id, or the user’s name exactly as assignedUser shows it.
date / startDate / endDatedateoptionalDate filtering.
sinceISO 8601 datetimeoptionalExclusive lower bound on timestamp. For incremental polling; see Polling patterns.
includeHistorybooleanoptional1 to nest the alert’s history audit log — chronological entries for comments, assignments, and status changes by operator. Any non-empty value turns it on, 0 included.

/alerts takes no pass-through filters: any other parameter is a 400, Unknown '<name>' parameter.

Response shape (per item in data)

{
  "id": 8421,
  "serial": 2318,
  "transactionId": 12345,
  "name": "AlertOverweight",
  "status": "open",
  "flagged": 1,
  "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",
  "scaleId": "IQ-001:A",
  "locationId": "PLANT-7"
}

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

On /transactions, /transactions/metrics and /events, any query parameter that isn’t one of the endpoint’s own parameters is treated as a pass-through kv pair, so a misspelled parameter name becomes a filter that matches nothing rather than an error. On /transactions/pdf, write each one with a data. prefix instead (data.customer=ACME), up to 8 per request; any other parameter it doesn’t recognize returns 400. 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 appeared in event metadata for the site within its query window (the last 90 days unless you pass dates). 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 (on /transactions/pdf, the part after data.) 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, with a message that starts Invalid filter key 'x-y'. (on /transactions/pdf, Invalid filter key name(s): data.x-y.) and restates this rule. Values can be any Latin-1 text (URL-encode as needed; see Text values are Latin-1).

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 (listing data.<key> names) that distinguish “you used a wrong key name” from “the value didn’t match anything.” Use this for debugging. It’s best-effort: when working it out would take too long, the response leaves it out.


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. To collect ticket PDFs, use the /tickets/pdf bookmark feed instead; it has its own loop.

Recommended pattern

  1. Log in once and reuse the token on every poll; don’t call /auth each time (see Log in once).
  2. Maintain a watermark — the timestamp of the most recent item you’ve processed.
  3. On each poll, query with ?since=<watermark> to fetch only items newer than your watermark.
  4. Update the watermark to the maximum timestamp in the response.
  5. On 429 TooManyRequests, honor the Retry-After header.
  6. 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-05:00"

# Subsequent polls — pass the prior max timestamp as `since`
GET /sw/api/transactions?serial=2318&since=2026-06-11T14:35:00-05:00
  → 200, response.data has only rows strictly newer than the watermark

By default, rows come oldest first by timestamp on /transactions and /alerts, but /events sorts by transactionId, so add orderBy=date there to get events in time order before you take the maximum timestamp.

Cadence

Use caseRecommended cadence
Near-real-time dashboard1–2 min
Analytics / reporting15–60 min
Daily reconciliationOnce daily
Historical backfillUse 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 — no Authorization header 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 /events or /alerts to 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 Authorization header 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 use Authorization: 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 (name values 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:

  1. Ignore unknown fields in response objects rather than erroring.
  2. Treat unknown enum values as opaque strings rather than crashing.
  3. 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: true response 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.

MessageTypical fix
No site named: Site serial number or locationId is required, or later in the pipeline serial or locationId parameter is requiredAdd ?serial=<your_site_serial> or ?locationId=<your_location_id>. See Choosing sites. (/tickets/pdf doesn’t need one.)
Invalid site serial, or serial must be a positive integer or comma-list of positive integersSerials are positive whole numbers, comma-separated: only digits, spaces and commas.
Invalid locationId, or locationId must be a location id of up to 64 Latin-1 characters, or a comma-list of up to 10Send your Command Cloud location ids as /sites shows them, comma-separated.
More than 10 sites: serial comma-list exceeds max 10 entries, locationId comma-list exceeds max 10 entries, or serial and locationId pick 12 sites; the maximum is 10 per requestSplit the sites across several requests.
location id(s) PLANT-7 match sites at more than one customer; use serial insteadThat location id matches sites under two or more different customers on your key (a site with no parent customer doesn’t count). Name those sites by serial. See Choosing sites.
Too many data.<key> filters: at most 8 per request/transactions/pdf takes up to 8 event-data filters. Drop some, or narrow the search another way (dates, tickets, scaleId).
Invalid 'customer' value: only Latin-1 (ISO 8859-1) characters are supportedRemove the character outside Latin-1 (an emoji, curly quotes, and so on). scaleId is exempt. See Text values are Latin-1.
Malformed date (e.g. 2026/06/10): Invalid 'startDate' valueUse ISO 8601 calendar date: YYYY-MM-DD.
'since' and 'date' are mutually exclusive — use 'since' for incremental polling and 'date'/'startDate'/'endDate' for date-bounded queriesSend one or the other. since composes with startDate/endDate.
Invalid kv key (special character, starts with digit, >64 chars): Invalid filter key '...'See key-name validation rules.
Unknown parameter: Unknown 'foo' parameter on /alerts; Unknown parameter(s): foo. Accepted parameters: ... on the PDF endpointsCheck the parameter list for the endpoint. On /transactions, /transactions/metrics and /events an unrecognized name is a pass-through filter instead, so a typo returns no rows rather than an error; /events/types and /events/keys ignore parameters they don’t use.

401 UnauthorizedAccess

Missing, malformed, or unrecognized bearer token. Re-check the Authorization header format. The message is Authentication failed. Invalid token. (/auth itself answers a wrong email or password with Invalid email or password. Please check your credentials and try again.)

Common causeTypical fix
Header missing entirelyAdd Authorization: Bearer <your_token>.
Persistent 401 on previously-working tokenToken rotated (your underlying key was regenerated). Log in once with POST /sw/api/auth to get a token for the new key, or wait for the replacement email from Dividia support.
Token doesn’t decodeWatch for trailing whitespace or truncation when copying. If the stored token is damaged, log in once with POST /sw/api/auth and store the new one.

403 ForbiddenRequest

Authenticated, but not permitted. Response includes structured fields naming the specific failure:

FieldMeaningFix
unauthorizedSerialsAccess denied for site serial(s): ... Listed serials are outside your access allowlist, or don’t exist (a deleted site counts as not existing). Both get the same answer, so a reply never reveals which serial numbers exist.Confirm the serial via GET /sw/api/sites; typos are the usual cause. Contact support to grant access.
unauthorizedLocationIdsAccess denied for location id(s): ... Listed location ids name none of the sites you can use, or no site at all (a deleted site counts as none). Both get the same answer. When you named both kinds, one message covers both, joined by ;.Compare with the locationId values GET /sw/api/sites returns; they match exactly, so check case and punctuation. Contact support to grant access.
apiDisabledSerialsSite(s) without API access: ... 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.
the same two arrays, listing whatever you namedUser has no API access for any site: your key has no sites assigned.Contact support to assign sites to your account.
contactFrom /auth: API access isn’t enabled on your account, or no sites are assigned (see POST /sw/api/auth).Contact support to enable API access on your account.

404 NotFound

The requested resource doesn’t exist. Note that /sw/api/transactions/pdf (and /sw/api/tickets/pdf?ticket=) intentionally returns 200 with a matches: 0 JSON body instead of 404 — that response carries a filter echo, and on /transactions/pdf diagnostic info (keyNeverSeen, valuesDidNotMatch) you can act on, when it can be worked out in time.

413 PayloadTooLarge

Query parameters over 4,096 characters (see the query string limit). 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 withMeaningFix
Duplicate requestYou sent an identical request to the same endpoint while a previous one was still being processed. The earlier one is served.Wait Retry-After seconds (10) and retry. Or de-duplicate client-side.
Too many requests for your account on 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.
Another /tickets/pdf request for this account is still running/tickets/pdf runs one request at a time per API user.Wait Retry-After seconds (60) and retry. Run one puller per API user.
Too many /tickets/pdf requests for your account/tickets/pdf per-user limit hit — 60 requests per minute.Honor Retry-After. Pull every 15 minutes or longer.
Too many login attemptsYou hit the /auth rate limit — 10 attempts per 15 minutes per IP.Log in once and reuse the token instead of logging in on every request or poll. Tokens don’t expire. See Log in once.
Too many requests, please try again later.The general per-IP limit — 1,000 requests per 15 minutes, all endpoints together.Honor Retry-After. Reduce polling cadence and batch sites into one request.

500 InternalServerError

Server error. Retry with exponential backoff. If persistent, contact support@dividia.net with the timestamp, the request URL, and the response body. On the PDF endpoints the 500 message is Failed to generate PDF.


Error code reference

HTTPtypeWhen
400BadRequestInvalid or missing parameter. The message field names the offending parameter.
400DateRangeTooLargeExplicit date range (or since lookback) exceeds the 366-day maximum. Split into multiple requests. See Dates and time zones.
400PageOutOfRange/transactions/pdf: a page past the last one, page N of M does not exist (T matches at limit L). It counts toward the rate limits, because the query has run.
400QueryTooBroadQuery too broad to complete in time (each of the request’s main database queries has a 20-second limit) — e.g. a key/value filter over a long range on a high-volume site. Message: Query exceeded the time limit. Narrow the date range (use date=YYYY-MM-DD for a single day, or a smaller startDate/endDate span), reduce pageSize, or remove broad key/value filters, then retry. /transactions/pdf, which takes no pageSize, says Query exceeded the time limit. Narrow the date range (use date=YYYY-MM-DD for a single day, or a smaller startDate/endDate span), pick fewer sites with serial or locationId, or remove broad data.<key> filters, then retry. /tickets/pdf says Query exceeded the time limit. Pick fewer sites with serial or locationId, or send a later from or after, then retry. It counts toward the rate limits.
401UnauthorizedAccessMissing or invalid bearer token. Re-run /sw/api/auth or contact support if your token was rotated.
403ForbiddenRequestAuthenticated but not permitted. See structured fields above.
404NotFoundResource genuinely not found. (Note: /transactions/pdf and /tickets/pdf?ticket= return 200 with matches: 0 instead.)
413PayloadTooLargeQuery parameters over 4,096 characters (see the query string limit). Trim filters or split into multiple requests.
429TooManyRequestsRate limit exceeded. Honor Retry-After. See Rate limits.
500InternalServerErrorServer 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. (Not a 401, or the 400 for a missing or malformed serial or locationId: those are answered before the request is logged.) 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.

Acceptance required. Before making your first API call, reply to your activation email confirming acceptance of the current Terms of Service. (Click-to-accept will replace the email reply in a future release.)

Support

ChannelDetail
Emailsupport@dividia.net
Phone+1 (866) 348-4342 · +1 (817) 288-1040
HoursMonday–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.