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.

Sunset: 30 September 2027. The legacy endpoints /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

What changes

Legacyv1
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"
The v1 endpoint accepts the same multipart form data. No changes to how you attach files.

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.

Questions? Contact support@qlim8.com or open an issue. We can help coordinate migrations for high-volume integrations.