Migrating from API v1 — API v2¶
Existing integrations speak the V1 REST API: /api/inventory without a
version prefix, three X_REST_* credentials, field codes like I4201.
API v2 lives under /api/v2, authenticates with a Bearer token and answers
in JSON:API format. This page lists what a V1 client has to touch when
migrating — and what deliberately keeps working.
The short version: field names stay — V1 codes remain valid
identifiers. Keys keep their value, MTAG and CTAG are
now called id (details). URLs,
authentication, filter syntax and the response envelope change.
Endpoints¶
V1 uses singular paths without a version, V2 plural paths under /api/v2:
| V1 | V2 |
|---|---|
GET /api/inventory |
GET /api/v2/inventories |
GET /api/calibration |
GET /api/v2/calibrations |
GET /api/customer |
GET /api/v2/customers |
GET /api/location |
GET /api/v2/locations |
GET /api/repair |
GET /api/v2/repairs |
GET /api/contact |
GET /api/v2/contacts |
GET /api/standard |
GET /api/v2/calibrations/{id}/standards (catalogue: /api/v2/test-standards) |
GET /api/result |
GET /api/v2/calibrations/{id}/results |
POST /api/result/update (bulk) |
PUT /api/v2/calibrations/{id}/results |
GET/POST /api/report/{id} |
GET /api/v2/calibrations/{id}/reports, POST …/reports/generate |
GET /api/document/{id} |
GET /api/v2/documents/{id} (file: …/download) |
GET /api/user / group / role |
GET /api/v2/users / groups / roles |
GET /api/app/version |
GET /api/v2/app/version |
POST /api/auth/login |
POST /api/v2/auth/token (issues a Bearer token) |
GET /api/{resource}/rules |
GET /api/v2/fields/{table} |
GET /api/{resource}/get/{field}/{value} |
GET /api/v2/…?filter[field]=value |
POST /api/{resource}/insert |
regular POST /api/v2/… |
PUT /api/{resource}/{id} / POST /api/{resource}/update |
regular PUT /api/v2/…/{id} |
IDs: MTAG, CTAG and the other tags¶
In V1 every record is keyed by a tag: MTAG on a device, CTAG on a
calibration, KTAG on a customer. In V2 that field is called id
everywhere. The value does not change, the data migration writes every
tag into the new column unaltered. An MTAG a legacy system stored still
points at the same device in V2.
| V1 tag | Record | V2 |
|---|---|---|
MTAG |
Device | id of /api/v2/inventories |
CTAG |
Calibration | id of /api/v2/calibrations |
KTAG |
Customer | id of /api/v2/customers |
LTAG |
Location | id of /api/v2/locations |
RTAG |
Repair | id of /api/v2/repairs |
When a tag points at a different record it is a foreign key, and V2 names
it after its target: the MTAG on a calibration becomes inventory_id, the
KTAG becomes customer_id.
The rule for this whole section:
Tag as the record's own key →
id. Tag as a reference to something else →…_id.
In a response the id sits next to type, one level above the fields:
{
"data": {
"id": "ea79f4c2-1b3d-4c88-9f21-7c0a5d13e001",
"type": "inventory",
"attributes": { "asset_number": "PM-100", "manufacturer": "FLUKE" }
}
}
Fetching a device by its MTAG¶
Several devices at once¶
Fetching a calibration by its CTAG¶
All calibrations of one device¶
# as a sub-resource of the device
curl "https://…/api/v2/inventories/$MTAG/calibrations" -H "$AUTH"
# or as a filter on the foreign key
curl "https://…/api/v2/calibrations?filter[inventory_id]=$MTAG" -H "$AUTH"
# the V1 code works here too: inventory_id is a registry field with v1_name MTAG
curl "https://…/api/v2/calibrations?filter[MTAG]=$MTAG" -H "$AUTH"
The same pattern applies to /api/v2/inventories/{mtag}/locations,
/api/v2/inventories/{mtag}/repairs and
/api/v2/customers/{ktag}/inventories.
One pitfall¶
filter[MTAG] on /api/v2/inventories returns every device
A filter on a field V2 does not know at that endpoint is skipped
silently. The response is then 200 with the complete inventory, not
404 and not 422. A client that simply takes data[0] works on the
wrong record from there on.
curl "https://…/api/v2/inventories?filter[MTAG]=$MTAG" -H "$AUTH"
# 200, "meta": { "total": 12480 } ← every device, filter discarded
curl "https://…/api/v2/inventories?filter[id]=$MTAG" -H "$AUTH"
# 200, "meta": { "total": 1 } ← correct
This hits exactly the case where the tag means the record's own key:
filter[MTAG] on /inventories, filter[CTAG] on /calibrations,
filter[KTAG] on /customers and so on. As a foreign key
(filter[MTAG] on /calibrations) the very same code works fine.
Fix: use filter[id]=…, or the path /api/v2/inventories/{mtag}.
Sanity check for any filter: compare meta.total with and without
the filter. The same number twice means the filter was discarded.
Cheat sheet¶
| Goal | V1 | V2 |
|---|---|---|
| Device by key | GET /api/inventory/{MTAG} |
GET /api/v2/inventories/{MTAG} |
| Device by key, as a filter | filter=[{"property":"MTAG","value":"…"}] |
filter[id]=… |
| Several devices | GET /api/inventory/get/MTAGs/a,b |
filter[id]=in:a,b |
| Calibration by key | GET /api/calibration/{CTAG} |
GET /api/v2/calibrations/{CTAG} |
| Measurements of a calibration | GET /api/calibration/get/CTAG/{CTAG}?getResult=1 |
GET /api/v2/calibrations/{CTAG}/results |
| Calibrations of a device | GET /api/calibration/get/MTAG/{MTAG} |
GET /api/v2/inventories/{MTAG}/calibrations |
| Customer by key | GET /api/customer/{KTAG} |
GET /api/v2/customers/{KTAG} |
| Devices of a customer | filter=[{"property":"ktag","value":"…"}] |
GET /api/v2/customers/{KTAG}/inventories |
Authentication¶
V1 accepts the three credentials as headers or query parameters. V2 switches to Bearer tokens:
# Issue a token (once, or on expiry)
curl -X POST "https://…/api/v2/auth/token" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "…", "device_name": "sync-tool"}'
# Then: token in the Authorization header
curl "https://…/api/v2/inventories" -H "Authorization: Bearer {token}"
As a transition, V2 keeps accepting the V1 credentials — as HTTP headers, all three together:
curl "https://…/api/v2/inventories" \
-H "X-REST-USERNAME: user@example.com" \
-H "X-REST-PASSWORD: …" \
-H "X-REST-API-KEY: …"
What no longer works
Credentials as query parameters (?HTTP_X_REST_USERNAME=…) and
authentication via an existing browser session are not accepted by
V2. Clients that ship credentials in the URL must switch to headers or
Bearer tokens. The X-REST header auth is deprecated and scheduled for
removal in 7.0 — new integrations should start with Bearer tokens
directly.
Response format¶
V1 answers with its own envelope, V2 with JSON:API:
Errors arrive as RFC 7807 problem JSON (Content-Type:
application/problem+json, status code + detail) or — for domain
conflicts — as {"error": {"status", "title", "detail"}}; there is no
success flag any more, the HTTP status is authoritative.
Transition: requesting the V1 envelope
With Accept: application/vnd.calserver.v1+json, V2 reshapes the
response envelope into the V1 format (success/message/totalCount).
This only affects the envelope, not the field names — and it is
deprecated (removal in 7.0). For field names see
V1 field names.
Filtering and sorting¶
V1 takes filters as a JSON array, V2 as query parameters per field:
Migration trap: LIKE vs. exact
Without an operator, V1 filters with LIKE (partial match); V2
without a prefix matches exactly. A filter carried over 1:1 finds
less in V2 — partial matches need the like: prefix.
Operator prefixes in V2: like:, notlike:, contains:, ncontains:,
startswith:, endswith:, ne:, gt:, gte:, lt:, lte:, in:a,b,c,
between:a,b, empty, nempty. Multiple conditions per field as an array
(filter[field][]=…&filter[field][]=…, AND-joined), filter_join[field]=or
switches to OR, filter_cs[field]=1 forces case sensitivity. Sorting:
sort=field ascending, sort=-field descending. Field names may be the V1
code everywhere (filter[I4201]=…).
Pagination¶
| V1 | V2 | |
|---|---|---|
| Parameters | limit / offset |
page[size] / page[number] |
| Default | 100 records | 25 records |
| Maximum | limit=all (everything) |
page[size]=100 |
The total count lives in meta.total (V1: data.totalCount). There is no
limit=all equivalent — fetch large datasets page by page.
Field names¶
Nothing to do: the V1 codes (I4201, C2301, K4601, …) remain valid
identifiers in filters, sorting and writes, and on migrated installations
they are even the factory default of the API name. Responses carry the codes
additionally on request (?include_v1_names=1 or header
X-CalServer-V1-Field-Names: 1). Details and edge cases:
V1 field names. The complete mapping of an
installation is served by GET /api/v2/fields/{table} (attributes
api_name, v1_name).