Sync feeds
Pagination by page number is fine for a person browsing, but it can skip or repeat rows when data changes underneath a loop. For keeping another system current, CardOffice offers keyset cursors: “everything that changed since X”.
The pattern is the same everywhere:
- Start with a timestamp, such as
1970-01-01T00:00:00Z. - Request the feed. Process the rows it returns, oldest first.
- Only after you have applied the batch, store
meta.nextSinceandmeta.nextSinceIdas your new position. - Repeat while
meta.hasMoreis true.
If you crash between steps 2 and 3, you simply repeat work. You never skip it. Treat the timestamp as an opaque string and send back exactly what you were given. Re-formatting it can lose sub-millisecond precision and make boundary rows replay forever.
Issued cards changed
Section titled “Issued cards changed”| Parameter | Description |
|---|---|
since |
ISO 8601 timestamp, required to select this form |
sinceId |
Tie-breaker returned by the previous call. Optional on the first call |
limit |
1 to 500, default 200 |
Rows are ordered by last change, so a revoked card reappears. Each row has
id, cardHolderId, externalId, externalUuid, serialNumber, status,
format, issueNumber, issuedAt, expiresAt, revokedAt,
revocationReason and updatedAt. Cards that were themselves mirrored from
the external system are excluded, so you never receive your own data back.
{ "data": [ { "id": "…", "serialNumber": "AC-000123", "issueNumber": 2, "status": "active" } ], "meta": { "nextSince": "2027-02-03 01:00:00.123456+00", "nextSinceId": "c2a4f0f6-1f1b-4b0a-9d7c-0d1b6e5e4a21", "hasMore": false }}Approved photos changed
Section titled “Approved photos changed”Every approved, current photo whose approval happened after your cursor.
limit is 1 to 200, default 50. Each row carries photoId, cardHolderId,
externalId, externalUuid, changedAt and a short-lived downloadUrl.
Only holders that carry an externalUuid are included.
Upsert a holder by external UUID
Section titled “Upsert a holder by external UUID”An idempotent create-or-update keyed on your system’s row UUID. Use this when
your own identifiers are not reliably unique: the UUID is the sync key, so two
people sharing an externalId cannot silently merge into one holder. The
second arrives, collides on the unique externalId, and comes back as 409
for a person to resolve. The body is a card holder with an
optional issuedAt timestamp, which records when your system issued the
person’s current card.
Reconcile holders
Section titled “Reconcile holders”A read-only drift check. Send up to 500 UUIDs:
{ "externalUuids": ["6f1b0c3e-…", "a02d9f71-…"] }For each UUID the answer says whether CardOffice holds that person and, if so,
their externalId, holderStatus and activeCard (issueNumber, status,
issuedAt). It changes nothing: reconciliation that repairs is just a second
writer with a delay.
Door-number allocation
Section titled “Door-number allocation”Where a door system owns credential numbers, CardOffice never invents them. Two endpoints carry that exchange:
- GET/card-holders/pending-door-numbers (scope
card_holders:read) lists holders created in CardOffice that the door system does not know yet. - POST/card-holders/{id}/door-numbers (scope
card_holders:write) records the numbers the door system allocated, or a permanent failure. A repeat for the same UUID returns200. Pointing a holder at a different UUID is refused with409, because numbers are never silently re-pointed.
One writer per field
Section titled “One writer per field”The reliable way to run a two-way sync is to give every field exactly one owner. The system of record for a field sends it; the other end only receives it. Two systems writing one column is not a sync, it is a race.
© 2026 Secure ID Ltd. All rights reserved.Documentation licence and terms