Migration guide: legacy → v1
Moving from the X-API-Key invoice endpoints to the /api/v1 REST API. Legacy endpoints remain available until 30 September 2027.
/api/invoices/bulk-upload and /api/invoices/status/:id will be removed on this date. Every legacy response already carries a Sunset header with this date.
What stays the same
- Your existing API key row is preserved — we do not delete or rotate it.
- The two legacy endpoints accept the same
X-API-Keyheader and return the same response shape until sunset. - Uploaded files are processed identically — same parsing, same categorization pipeline.
What changes
| Legacy | v1 | |
|---|---|---|
| Base URL | https://app.qlim8.com |
https://api.qlim8.com/api/v1 (live)https://sandbox.api.qlim8.com/api/v1 (sandbox) |
| Auth header | X-API-Key: <64-hex> |
Authorization: Bearer qk_live_<64-hex> |
| Error format | { "message": "…" } |
RFC 7807: { "type", "title", "status", "code", "detail" } |
| Pagination | offset / limit | Opaque cursor (?cursor=…) |
| Tier required | Any | Enterprise (fullApiAccess) |
| Rate limits | Per-tenant express-rate-limit |
Per-key rate-limiter-flexible, RFC headers returned |
Step 1 — Mint a v1 key
In the app: Collectors → API Keys → Generate Key. Pick the minimum set of scopes you need — activities:bulk is the v1 equivalent of the legacy invoice-upload permission.
Or programmatically, once you already have any v1 key:
curl -X POST https://api.qlim8.com/api/v1/api-keys \
-H "Authorization: Bearer qk_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"prod-import","scopes":["activities:bulk"]}'
Step 2 — Update your client
Before (legacy):
curl -X POST https://app.qlim8.com/api/invoices/bulk-upload \
-H "X-API-Key: <legacy-key>" \
-F "files=@invoice.pdf"
After (v1):
curl -X POST https://api.qlim8.com/api/v1/activities/bulk \
-H "Authorization: Bearer qk_live_<new-key>" \
-F "files=@invoice.pdf"
Step 3 — Switch to RFC 7807 error handling
Legacy error handling:
// Legacy
if (!res.ok) throw new Error((await res.json()).message);
v1 error handling:
// v1 — RFC 7807
if (!res.ok) {
const problem = await res.json();
throw new Error(
`[${problem.code ?? 'unknown'}] ${problem.title}: ${problem.detail ?? ''}`
);
}
The code field is a machine-readable string (e.g. "auth.key_revoked", "quota.exceeded") — prefer branching on code rather than status for fine-grained handling.
Step 4 — Webhooks (optional)
Once on v1 you can replace polling with event subscriptions:
curl -X POST https://api.qlim8.com/api/v1/webhooks \
-H "Authorization: Bearer qk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/qlim8",
"events": ["supplier.connected", "report.generation_completed"],
"description": "Production webhook"
}'
The response includes a signing_secret returned once — store it in your secret manager. Deliveries carry X-Qlim8-Signature: t=<unix>,v1=<hex-hmac>; the signed payload is "<t>.<rawBody>":
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, sig_header: str, secret: str, tolerance: int = 30) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False # replay protection
signed_payload = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
See the webhook signing guide for the full scheme and event catalog.
Sunset date
Legacy endpoints carry a Deprecation: true and Sunset: Tue, 30 Sep 2027 00:00:00 GMT header on every response. You can monitor this programmatically — parse the Sunset header and alert when fewer than 90 days remain.