Version française

KebecPhoto Lab Pull API v1

Machine-to-machine integration guide for Photolab Yves Thomas — version 1.1 — July 2026

1. Introduction

The Lab Pull API lets your system fetch print batches from KebecPhoto over HTTPS — the manifest(s) plus every print-ready image file — instead of waiting for our SFTP deposit into your /incoming folder. The manifest served by this API is the fully-detailed SoumissionLabo XML (see §7). It differs from the simplified Timestone CSV we deposit on SFTP: the image files and their filenames are byte-identical across both channels, but the manifest is not — SFTP carries the Timestone CSV, this API carries the SoumissionLabo XML.

During the transition period the two channels run in parallel (dual-rail): we keep depositing to your SFTP exactly as today, and the same batches are simultaneously available through this API. Once your integration is validated in production, we will (by mutual agreement) stop the SFTP deposit; the API then becomes the only delivery channel. The switch is instantly reversible on our side.

Byte-identical images. The image files served by this API are exactly the bytes the SFTP deposit ships — same crops, same filenames. The manifest differs by channel: SFTP carries the simplified Timestone CSV, this API carries the fully-detailed SoumissionLabo XML — same orders, different envelope. Every file (and manifest) comes with its size and SHA-256 checksum so you can verify each download.

What the API gives you:

2. Quick start

# 1. List batches awaiting pickup (oldest first)
curl -H "Authorization: Bearer $KEY" \
  "$BASE/batches?status=UPLOADED"

# 2. Get a batch's file inventory
curl -H "Authorization: Bearer $KEY" \
  "$BASE/batches/{batchId}"

# 3. Download the manifest(s)
curl -OJ -H "Authorization: Bearer $KEY" \
  "$BASE/batches/{batchId}/manifests/{name}"

# 4. Download every file (max 4 in parallel)
curl -OJ -H "Authorization: Bearer $KEY" \
  "$BASE/batches/{batchId}/files/{filename}"

# 5. Confirm receipt (idempotent)
curl -X POST -H "Authorization: Bearer $KEY" \
  "$BASE/batches/{batchId}/confirm"

3. Environments & base URLs

EnvironmentBase URLAPI key prefix
Staging (integration testing)https://staging.kebecphoto.com/api/lab/v1kplab_stg_…
Productionhttps://app.kebecphoto.com/api/lab/v1kplab_prod_…

Keys are environment-bound: a staging key is cryptographically inert against production and vice versa (the server rejects a mismatched environment tag before any lookup). You will receive a staging key first; we move to production once your end-to-end flow is validated against staging test batches.

4. Authentication

Every request carries your API key in the Authorization header:

Authorization: Bearer kplab_prod_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Authentication failures

Every authentication failure — missing header, malformed key, unknown key, revoked key, expired key — returns the same uniform response (deliberately: no information leaks about key state):

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json

{"error":"unauthorized"}
Failure backoff. Repeated failures with a well-formed key (wrong, revoked, expired, or wrong-environment) from one IP — more than 5 per minute — trigger an escalating cooldown (1 min → 5 min → 15 min → 60 min) during which requests get 429 with a Retry-After header. (Requests without a syntactically valid key always get a plain 401.) A correctly configured client never encounters this; if you do, stop retrying, check the key, and respect Retry-After.

5. Conventions

6. Endpoint reference

6.1 List batches

GET/batches

Returns batches visible to your lab, oldest first by creation time (createdAt ascending, deterministic id tiebreak), with cursor pagination. Note this is creation order, not availability order — a deferred school batch that becomes available tonight can appear ahead of batches created after it. Only batches in a deliverable state are ever listed — you will never see internal, failed, or superseded batches here.

Query parameterValuesDefaultDescription
statusUPLOADED · CONFIRMED · allUPLOADEDUPLOADED = available, not yet confirmed by you. CONFIRMED = already acknowledged. Any other value falls back to the default.
sinceISO 8601—Only batches whose files became available at/after this instant. This is availability time, not creation time: a school batch created three days ago but uploaded tonight WILL appear. An unparseable value is ignored.
cursorbatch id—Resume after this batch id — only ever send the nextCursor from the immediately preceding response. A malformed value is ignored (you get the first page); an unknown id yields an empty page (hasMore: false) — restart without a cursor on your next poll.
limit1–10020Page size (values outside the range are clamped).

Response 200:

{
  "batches": [
    {
      "id": "cmkabc1230000xyz456def789",
      "status": "UPLOADED",
      "createdAt":   "2026-06-10T14:00:00.000Z",
      "submittedAt": "2026-06-10T14:05:12.000Z",
      "confirmedAt": null,
      "orderCount": 3,
      "itemCount": 12,
      "isConsolidated": false,
      "trigger": "DATED_DEADLINE",
      "filesAvailable": true
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
FieldTypeMeaning
idstringBatch identifier — use it in every other endpoint.
statusstringUPLOADED or CONFIRMED (see lifecycle).
createdAttimestampWhen the batch was created on our side (can be days before availability for school-delivery consolidation).
submittedAttimestamp · nullWhen the batch finished uploading / became deliverable.
confirmedAttimestamp · nullWhen YOU confirmed receipt (null until then).
orderCount / itemCountnumberOrders and print line-items in the batch (matches the manifest's <NombreCommandes>/<NombreArticles>).
isConsolidatedbooleanInformational: true when the batch was produced by nightly consolidation or manually triggered for immediate processing (cross-check trigger: NIGHTLY_RUN vs MANUAL). Safe to ignore.
triggerstring · nullWhat caused submission: DATED_DEADLINE, NIGHTLY_RUN, SAME_DAY_HOME, MANUAL, or null (legacy).
filesAvailablebooleanfalse = nothing was ever published for API download (the batch pre-dates the API) — skip it. Note: in the LIST this flag does not reflect post-retention purge; purge is reported by the DETAIL endpoint (purged: true, whose own filesAvailable then reads false).
nextCursor / hasMorestring · null / booleanPagination: pass nextCursor back as cursor while hasMore is true.

6.2 Batch detail (file inventory)

GET/batches/{batchId}

All list fields above, plus the complete inventory: manifests and image files, each with size and SHA-256.

{
  "id": "cmkabc1230000xyz456def789",
  "status": "UPLOADED",
  "…": "…(all list fields)…",
  "filesAvailable": true,
  "purged": false,
  "manifests": [
    {
      "name": "BATCH-2026-06-10-cmkabc1230000xyz456def789.xml",
      "groupKey": "batch",
      "sizeBytes": 8421,
      "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    }
  ],
  "files": [
    {
      "filename": "KP-20260610-A1B2C3_001_YT-4X6-MAT.jpg",
      "groupKey": "batch",
      "orderRef": "KP-20260610-A1B2C3",
      "quantity": 5,
      "lineType": "STANDARD",
      "side": null,
      "sizeBytes": 2048312,
      "sha256": "ab12cd…"
    },
    {
      "filename": "KP-20260610-A1B2C3_007_YT-CARTE_CARD.jpg",
      "side": null, "…": "…"
    },
    {
      "filename": "KP-20260610-A1B2C3_012_YT-CARTE_FRONT.jpg",
      "side": "FRONT", "…": "…"
    },
    {
      "filename": "KP-20260610-A1B2C3_012_YT-CARTE_BACK.jpg",
      "side": "BACK", "…": "…"
    }
  ]
}
FieldTypeMeaning
manifests[].namestringManifest filename — pass it verbatim to the manifest download endpoint. One per delivery group (see groupKey).
files[].filenamestringExact lab filename — identical to the SFTP deposit and to the manifest's <FichierImage> / <FichierImageVerso> values. Pass it verbatim to the file download endpoint.
groupKeystringDelivery group, mirroring the SFTP folder layout: batch = orders grouped to the studio (your batch/ folder); ORDER-… = one direct-to-home shipment per group (your orders/ folder).
orderRefstring · nullHuman order number the file belongs to (also embedded in the filename).
quantitynumber · nullPrint quantity for the line (one file may print N copies — matches the manifest's <Quantite>).
lineTypestring · nullSTANDARD (single print / sports card) or SHEET (pre-imposed composite sheet, print as-is).
sidestring · nullSports-card faces only: FRONT / BACK for the legacy recto/verso pair (a front-only card carries FRONT). Null for the merged _CARD file (the filename suffix identifies it) and for everything else.
sizeBytes / sha256number / stringExact byte size + SHA-256 (hex) of the file — verify after download.
purgedbooleanTrue when the files passed retention and were removed (see retention) — inventory metadata remains, downloads 404.

Special status code — 410 Gone: if a batch you previously saw was replaced by a consolidated batch before pickup, its detail returns:

{"error":"batch_superseded","supersededByBatchId":"cmk…"}
410 means DO NOT PRINT this batch. Discard anything you may have fetched for it and process the replacing batch (supersededByBatchId, string or null) instead — it appears in your normal list. If supersededByBatchId is null, simply discard and rely on your normal status=UPLOADED polling. If file downloads start returning 404 mid-drain, re-fetch the batch detail — a 410 there is this case.

6.3 Download a manifest

GET/batches/{batchId}/manifests/{name}

Streams the manifest — the fully-detailed SoumissionLabo XML (see §7 for the schema). UTF-8, opening with an <?xml version="1.0" encoding="UTF-8"?> declaration. This is the API's manifest format; the SFTP deposit instead carries the simplified Timestone CSV.

Response headers on success (200):

Content-Type: application/xml
Content-Length: 8421
Content-Disposition: attachment; filename="BATCH-2026-06-10-….xml"
X-Checksum-SHA256: 9f86d081…
Cache-Control: private, no-store

404 {"error":"not_found"} for an unknown name, a purged batch, or a batch that is not (or no longer) deliverable.

6.4 Download a file

GET/batches/{batchId}/files/{filename}

Streams one print-ready JPEG, byte-identical to the SFTP deposit. Same headers as above with Content-Type: image/jpeg.

6.5 Confirm receipt

POST/batches/{batchId}/confirm

Call once ALL files of the batch are downloaded and verified. The batch flips to CONFIRMED (and drops out of your default status=UPLOADED list). Empty request body.

Response 200:

{
  "id": "cmkabc1230000xyz456def789",
  "status": "CONFIRMED",
  "confirmedAt": "2026-06-10T15:42:08.000Z",
  "alreadyConfirmed": false
}

Optional verification report (since 2026-06-12). A client that verifies its downloads (such as the KebecPhoto Labo desktop app) may attach a JSON body (≤ 1 KB):

{
  "agentVersion": "1.0.0",
  "mode": "auto",
  "imagesVerified": 135,
  "manifestsVerified": 2,
  "bytesVerified": 287345234,
  "sha256MismatchCount": 0,
  "durationMs": 184233
}
Confirmation matters: it is our acknowledgement signal (it feeds dashboards and, eventually, the SFTP cutover decision), and it starts the retention clock after which files are purged. The server does not track your downloads — confirming is purely your attestation. Confirm only when you truly hold (and verified) everything.

7. Data reference

Filename grammar

{orderRef}_{seq}_{labSku}[_{FRONT|BACK|CARD}].jpg
 KP-20260610-A1B2C3 _ 001 _ YT-4X6-MAT          .jpg   → standard print
 KP-20260610-A1B2C3 _ 007 _ YT-CARTE _ CARD     .jpg   → MERGED double-sided card (default)
 KP-20260610-A1B2C3 _ 012 _ YT-CARTE _ FRONT    .jpg   → card, recto (legacy pair / front-only)
 KP-20260610-A1B2C3 _ 012 _ YT-CARTE _ BACK     .jpg   → card, verso (legacy pair)

Sports cards (recto/verso)

Default — merged _CARD file: a double-sided card now deposits ONE file: front and back side-by-side on a single double-width image (e.g. a 2.5×3.5 in card → one 5×3.5 in-class file). The density (DPI) tag written into the JPEG is the physical truth of the format — the file IS the final sheet, pre-rendered at print dimensions: print verbatim, no cropping or resizing, then cut at the centre line. In the SoumissionLabo XML the merged line is ONE <Article> with a single <FichierImage> (never a <FichierImageVerso>) and no size tags (<TailleCm>/<TaillePo> omitted — the single-face size would be wrong for the double-width sheet). In the API inventory: a single entry, side: null, identified by the _CARD suffix.

Legacy — _FRONT/_BACK pair: card lines predating the migration still deposit TWO files forming ONE <Article> carrying both <FichierImage> (recto) and <FichierImageVerso> (verso), so one billable card remains a single article. In the API inventory they appear as two entries sharing the same orderRef, distinguished by side: "FRONT" / "BACK". Both are pre-rendered at print dimensions — print verbatim, no cropping. (A front-only card stays a single _FRONT file.)

Imposed sheets

lineType: "SHEET" entries are pre-imposed composite JPEGs (300 DPI, ready to print as-is); the manifest article carries <Recadrage applique="non" /> and no size elements — exactly as described in our June 2 exchange about feuilles imposées.

Delivery groups (groupKey)

groupKeySFTP equivalentMeaning
batchbatch/ folderSchool-delivery orders grouped for the studio (one manifest for the group; studio address in <Livraison type="ecole">).
ORDER-{date}-{ref}orders/ folderOne home-delivery order shipped directly to the parent (its own manifest; address in <Livraison type="expeditionDirecte">).

A mixed batch contains the batch group AND one ORDER-… group per home-delivery order — download every manifest listed in manifests[].

The manifest (SoumissionLabo XML)

The API manifest is the fully-detailed SoumissionLabo XML — richer than the simplified Timestone CSV we deposit on SFTP; the two are not interchangeable. Root element <SoumissionLabo>; one <Article> per print line (a merged _CARD card is one article with a single <FichierImage> and no size tags; a legacy pair is one article with both <FichierImage> and <FichierImageVerso>). Family orders mix children within one <Commande>, each article carrying its own student. UTF-8, with an XML declaration.

ElementWhereContent
<Lot>rootBatch header: <NumeroLot>, <DateCreation>, <NombreCommandes>, <NombreArticles>, <NombreTirages>.
<Studio>rootPhotographer: <Nom>, <CodeStudio>, <CompteLaboYT> (your lab account number for this studio).
<Commande>under <Commandes>One per order: <NumeroCommande> + the blocks below.
<Client>orderCustomer <Nom>, <Courriel>, <Telephone>.
<Livraison type="…">orderecole (school/studio grouping — the batch group), expeditionDirecte (direct-to-parent — an ORDER-… group), or numerique (digital, no address). Address children: <Ligne1>, <Ville>, <Province>, <CodePostal>, <Pays>.
<Article>under <Articles>One per print line. Key children: <CodeProduit> (YOUR product code), <Quantite>, <FichierImage> (+ <FichierImageVerso> for LEGACY card-pair versos only — never on a merged _CARD line), <Recadrage applique="oui|non"> with <X>/<Y>/<Largeur>/<Hauteur> when cropped. <TailleCm>/<TaillePo> are omitted on merged _CARD lines (and on imposed sheets).
optional article tagsarticle<Decor> (background design name), <GroupeSource> (group-photo source group), <CommentairesCommande> (parent order comment, when enabled).

Which manifest you get follows the delivery groups (groupKey): the batch group's manifest carries <Livraison type="ecole">; each ORDER-… group's manifest carries <Livraison type="expeditionDirecte">. (The simplified Timestone CSV is the format we deposit on your SFTP; this API always serves the detailed SoumissionLabo XML. We would notify you before changing either.)

8. Recommended integration flow

Poll every 5 minutes; persist two things on your side: the set of batch ids you have fully processed, and the timestamp of your last successful poll.

every 5 minutes:
  since = last_successful_poll − 1 hour          # overlap on purpose; dedupe by batch id
  page  = GET /batches?status=UPLOADED&since={since}
  loop:
    for batch in page.batches:
      if batch.id in processed or not batch.filesAvailable: continue
      detail = GET /batches/{batch.id}            # 410 → mark void, print superseding batch instead
      for m in detail.manifests: download + verify sha256
      for f in detail.files:     download + verify sha256   # ≤ 4 in parallel
      hand off to production pipeline
      POST /batches/{batch.id}/confirm            # idempotent — safe to repeat after a crash
      mark batch.id processed
    if not page.hasMore: break
    page = GET /batches?…&cursor={page.nextCursor}
  last_successful_poll = now

9. Error catalog

HTTPerror codeMeaningWhat to do
401unauthorizedMissing/invalid/revoked/expired key (uniform — no detail by design).Verify the key and environment; contact Francis if it persists.
404not_foundUnknown batch/manifest/file, purged files, or a resource outside your lab's scope.Skip; re-list. For a purged batch, nothing to fetch.
405method_not_allowedWrong HTTP method (e.g. GET on confirm).Fix the request.
409invalid_statusBatch exists but cannot be confirmed right now.Re-check the batch via detail; contact Francis if persistent.
410batch_supersededDo not print. Batch replaced by supersededByBatchId.Void locally; process the replacing batch.
413payload_too_largeA request body exceeded 1 KB (the confirm report cap).Keep the optional report under 1 KB; send empty bodies elsewhere.
429rate_limitedEndpoint rate limit exceeded (or auth-failure cooldown).Wait Retry-After seconds (also in the JSON as retry_after).
429too_many_concurrent_streamsMore than 16 simultaneous file downloads.Wait for a transfer to complete; retry after Retry-After.
503too_many_concurrent_streamsTransient server-side inability to track download slots.Retry after Retry-After (seconds).
5xx—Transient server error.Exponential backoff and retry; alert us if sustained.

10. Rate limits & concurrency

Limits are per lab (all of your active keys share the same buckets), per minute (fixed window). The poll/confirm limits sit far above the documented cadence. The file bucket (600/min) paces large batches — while draining a big batch you should not normally see a 429 rate_limited; if you do, honour Retry-After and continue.

EndpointLimit
GET /batches (list)120 / min
GET /batches/{id} (detail)240 / min
GET …/manifests/{name}120 / min
GET …/files/{filename}600 / min + max 16 concurrent transfers
POST …/confirm60 / min

Batch splitting uses a limit of 500 image lines, with manifests and some additional files added separately; download duration depends on data volume and transfer speed.

An additional per-IP safety cap (sized well above the sum of the buckets) protects the API pre-authentication; it is unreachable at the documented limits but applies per source IP — run your poller as a single coordinated client rather than many parallel processes behind one egress IP.

11. Data retention & batch lifecycle

12. Versioning & support