Migrating from v1

v2 provides API-key replacements for reading, redemption, reporting, order-page configuration, and negotiated compatibility issuance. It also adds a protected headless checkout architecture that v1 did not have: authoritative catalog and quote, unpaid po_… orders, Lifepeaks-hosted QuickPay, canonical payment state, and signed webhooks.

Do not mechanically translate v1 create-and-capture into a new public storefront. Existing compatibility operations have contract gates, and v2 capture does not prove buyer payment. New buyer journeys should follow Headless checkout.

v1 is not being switched off. Migrate when it suits you.

What changes, in one screen

v1v2
CredentialOAuth2 access token from POST /tokenAPI key, Authorization: Bearer lp_live_…
Token lifetime24 h, refreshed with a refresh tokenNo expiry unless you set one at minting
PermissionsOAuth client scope (admin) plus per-user rightsLeast-privilege resource scopes on the key; settlement also needs an entitlement
Companycompany parameter on some calls; agencies act for manyUnchanged — optional company parameter, defaulting to the key's own company
Success body{"status": "OK", "response": …}The payload itself, unwrapped
Errorsstatus field plus a message{"error": {"code", "message", "fields"}} with real HTTP status codes
Read callsMostly POST with a JSON bodyGET with query parameters
LanguagePath prefix (/en/, /se/, /de/)Unchanged — the same prefixes apply
MoneyDecimal kroner everywhereUnchanged on compatibility and item routes; whole øre on the headless path
What you can sellGift cards only, one at a timeGift cards, experiences, event tickets and special offers, through one catalog and one order route

What v2 sells that v1 did not

v1 issues gift cards. v2's headless path sells four product families from one catalog, and every one of them is quoted, ordered, paid and fulfilled by the same three calls:

FamilytypeProduct idOption id
Value gift cardgift_cardgift_card_value
Experiencegift_card_variantgcv_<id>gcvo_<id>
Event ticketevent_ticketevt_<id>evto_<id>
Special offerspecial_offerso_<id>sov_<id>

None of this changes anything a v1 integration already does. POST /v2/items still issues gift-card value and nothing else, and the item, redemption and reporting routes read every family by code exactly as they always have — the type filter on GET /v2/items already accepts TICKET, SPECIALOFFER and BENEFITDEAL.

Three things a Lifepeaks operator issues by hand are deliberately outside the public API: ticket coupons, Saved Special Vouchers and New Special Vouchers. Benefit deals are read and redeemed by code, never bought. All four still appear at GET /v2/items and redeem at POST /v2/items/{code}/claim, and any of them prints at GET /v2/items/{code}/document.pdf.

Authentication

v1 uses an OAuth2 password grant: your client_id / client_secret plus a Lifepeaks username and password are exchanged at POST /token for a 24-hour access token, refreshed with a refresh token.

v2 replaces all of that with a long-lived API key.

bash
curl -X POST https://api.lifepeaks.dk/v2/api-keys \
  -H "Authorization: Bearer lp_live_<an-existing-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Booking system",
    "scopes": ["items:read", "items:write", "orders:create"],
    "expires_in_days": 365
  }'

expires_in_days is optional — an integer from 1 to 365, counted from creation. Omit it and the key never expires; the response's expires_at is then null. There is no refresh flow: rotate by minting a new key and revoking the old one with DELETE /v2/api-keys/{id}.

Your first key comes from the admin panel (Settings > API Keys); a key can only mint keys whose scopes it already holds. See Authentication for the full lifecycle.

Scope mapping

v1 has effectively one OAuth client scope, admin, which unlocks every action; access is then narrowed per action by user rights on the logged-in account (agency_user, cancel_user, right_disable_listing_items). v2 splits that broad access into explicit resource scopes and drops the user-rights layer — a key is not a user.

v1 actionv1 requirementv2 endpointv2 scope
POST /action/listscope adminGET /v2/itemsitems:read
GET /item/{code}scope adminGET /v2/items/{code}items:read
POST /item/{code}/claimscope admin, blocked for agency_userPOST /v2/items/{code}/claimitems:write
POST /item/{code}/refundscope admin, blocked for agency_userPOST /v2/items/{code}/refunditems:write
POST /item/{code}/cancelscope admin, needs agency_user or cancel_userPOST /v2/items/{code}/cancelitems:write
POST /item/{code}/activatescope adminPOST /v2/items/{code}/activateitems:write
POST /item/{code}/resendscope adminPOST /v2/items/{code}/resenditems:write
POST /action/create-gcscope admin, needs agency_userPOST /v2/items compatibility routeorders:create + orders:settle + active contract
POST /order/{order_id}/captureauthenticated userPOST /v2/orders/{order_id}/capture compatibility routeorders:settle + active contract
POST /order/{order_id}/claimscope adminPOST /v2/orders/{order_id}/claimitems:write
POST /event/listscope admin, blocked for agency_userGET /v2/eventsitems:read
POST /event/{slug}/participantsscope admin, blocked for agency_userGET /v2/events/{slug}/participantsreports:read
GET /list/gc-value-modificationsscope adminGET /v2/promotionsitems:read
POST /action/analyticsscope adminGET /v2/reports/analyticsreports:read
POST /action/claimed-itemsscope adminGET /v2/reports/claimed-itemsreports:read
GET /action/subscribersungatedGET /v2/subscribersreports:read
GET /action/pingscope adminGET /v2/healthauthenticated API key

Two consequences of dropping the user-rights layer are worth planning for:

  • Cancel is no longer creator-restricted. v1 additionally narrows cancel to items the calling user created, unless the user holds cancel_user. A key has no creator, so the company boundary is the whole check: a key with items:write can cancel any of that company's cancellable items.
  • GET /v2/subscribers is now gated. In v1 the endpoint is ungated and any authenticated caller can read it. v2 requires reports:read, because the rows are names and email addresses.
  • Credential management is separate. Listing keys needs credentials:read; issuing or revoking them needs credentials:write.

v1's gc-value-modifications list is GET /v2/promotions in v2. The rows and the rules are the same; only the name changed, to the word a partner would actually use for them.

Tenancy: company works as it always did

In v1 an agency account passes company (a slug) to act for one of its assigned companies. v2 keeps that exactly as it was: pass company and the request acts on that company.

  • Omit it and the request acts on the API key's own company. That is the default and covers every single-company integration.
  • Pass it as the company query parameter and the request acts on that company, provided the slug is your own company or one Lifepeaks has assigned to yours. A POST or a PATCH may carry the same slug as a company field in its JSON body instead, and the query string wins when both carry it. The one exception is POST /v2/quotes, whose body accepts items alone — name the company in its query string there.

One key can therefore serve an agency across all of its assigned companies, just as one v1 account did. A key per company remains equally valid. Only the API-key endpoints and GET /v2/health take no company parameter; they always act on the key's own company.

Anything outside that boundary is 404 not_found, never 403 — an unknown slug, an inactive company, and a company that belongs to someone else all answer the same way. v2 deliberately does not distinguish "does not exist" from "belongs to someone else".

Idempotent replays follow the same boundary. Compatibility operations record X-Request-Guid against the acting company. New durable headless operations require Idempotency-Key, also within the acting-company boundary.

Redemption of expired items

v2 follows the company-level claim_expired setting exactly:

  • claim_expired >= 0 — expired items stay redeemable for that many months after expiry, on every channel including v2. Past the grace period the claim fails with the company's grace-period message.
  • claim_expired = -1 — the company allows expired items to be redeemed, and v2 allows it.

The per-user claim_expired setting never applies to an API key, because a key is not a user and has no per-user settings to read. In v1 the user setting only takes effect when the company setting is -1, and it defaults to -1 itself, so for most companies the two behave identically and nothing changes on migration.

Creating gift cards

POST /v2/items is a negotiated compatibility operation for invoice-settled integrations. It preserves the familiar v1 field names, validation, campaign range rules, and response shape, but requires orders:create, orders:settle, and an active invoice-settlement contract. Send company in the query string or in the JSON body; the query string wins when both carry it.

amount here keeps v1's unit: a decimal in the company's currency, so 500 is a DKK 500.00 card. Existing v1 amounts move across unchanged. The headless routes are the ones that differ — see the callout at the top of this page.

For a new buyer-facing frontend, do not use this route. Use POST /v2/quotes, create an unpaid POST /v2/orders, and redirect through Lifepeaks-hosted QuickPay. Lifepeaks then controls payment, commission, and fulfillment. Those two calls take amount in whole øre, so multiply a v1 amount by 100.

The POS/PMS path stays on v1

v1 settles differently for a caller registered as a POS/PMS OAuth client: pay-on-location commission and payment type instead of invoice settlement. That behaviour is selected from the OAuth client id, which an API key does not have. The v2 compatibility create route is invoice-settled only and contract-gated.

If your integration is registered as a POS/PMS client, keep its create path on v1 until Lifepeaks agrees a v2 contract. Reading, redeeming, refunding, and reporting can move independently. Agency/marketplace, POS/PMS, pay-on-location, external-processor, and non-Lifepeaks settlement designs are negotiated and entitlement-gated; an API scope alone does not enable them.

Delivery: the quirk that surprises everyone

Attribution on created rows

v1 stamps the creating user on every created row. A key is not a user, so v2 records created_by only when the key was minted by a backend user, and null otherwise. The key itself is recorded on the payment instead, so a row can still be traced back to the integration that created it. In reports, claimed_by_username is likewise null for anything an API key redeemed.

Compatibility capture is not payment capture

v1's capture finds the uncaptured order by the user who created it — one user cannot capture another's order, even inside the same company.

The v2 compatibility route narrows by company, but it only activates an invoice-settled gift-card order that was created with capture: false. It does not capture card funds and must not be used as evidence that a buyer paid. It requires orders:settle plus an active invoice-settlement contract.

Do not migrate the two halves of a public v1 payment workflow independently. For protected headless checkout, a browser redirect never proves payment; use the po_… order's canonical payment state and signed webhooks. An already activated compatibility order answers 404 because it is no longer in the uncaptured set.

Wonderbox / GoDream passthrough

The passthrough is supported on v2. v1 gates it on the logged-in user's claimable companies; v2 gates it on the company you are acting as, so a company carrying wonderbox_implementation can look up and redeem a GoDream code on v2 exactly as on v1. For a company without the flag, a numeric code is treated as an ordinary code.

One thing to keep in mind when reconciling: responses for vendor codes are synthesized from the vendor's API, exactly as in v1. Those rows are bounded by what your company's vendor account returns, not by Lifepeaks' own item filtering — so a vendor code can appear in a lookup even though no Lifepeaks item exists for it yet, and vendor rows will not necessarily line up with a GET /v2/items search over local data.

Response shape

Unwrap one level and you are most of the way there. v1:

json
{
  "status": "OK",
  "response": { "items": [  ], "itemsCount": 1 }
}

v2:

json
{ "object": "list", "data": [  ], "has_more": false, "next_cursor": null }

Row shapes inside are unchanged, including the reciever_name / reciever_email misspelling, which is kept deliberately so parsers do not have to change.

Three response details differ beyond the unwrapping:

  • Every v2 list uses the same envelope. The rows are under data, never items, events or participants; there is no itemsCount, eventsCount or participantCount — count data — and next_cursor is the cursor for the next page.
  • Subscribers were a bare array in v1's response. v2 returns the list envelope, like every other list.
  • filter is a v1 development-server echo (api-dev.lifepeaks.dk). v2 never returns it.

Errors are no longer carried inside a 200. Check the HTTP status, then error.code: unauthorized (401), insufficient_scope (403), not_found (404), validation_failed (422). Field-level detail arrives in error.fields on 422 only.

Idempotency

v1 and the v2 compatibility operations use the X-Request-Guid replay header. Every successful response to a compatibility mutation (POST /v2/items, compatibility capture, order claim, and the five item write operations) carries that response header. Send it back when retrying the identical request to replay the recorded response.

New durable headless operations use a required Idempotency-Key instead: order creation, order refund, checkout-session creation, negotiated settlement, greeting-image finalize, every PDF-template write (upload intent, finalize, activate, delete), webhook registration/rotation/replay and ping, and order-page draft create/publish/restore. Generate and persist a stable key before the first call. Use independent keys for independent operations, and never generate a new key inside a retry loop.

Two differences from v1 worth knowing:

  • v2 replays only for the company the original request acted on, and only after authentication — a GUID is not a bearer credential on v2.
  • v2 compatibility operations have no silent time-window replay (v1 replays an identical request from the same client within a few seconds even without the header). On a compatibility operation, no header means the request executes. Headless durable operations reject a missing Idempotency-Key.

Suggested order of migration

  1. Mint a key with items:read only and re-point your read calls (GET /v2/items, GET /v2/items/{code}).
  2. Add reports:read and move analytics, claimed items and subscribers.
  3. Add items:write and move redemption, refund, cancel, activate and resend.
  4. Keep v1 creation in place until Lifepeaks confirms whether your existing invoice/POS/PMS contract is eligible for a v2 compatibility route.
  5. Design any new customer storefront as a separate protected headless integration with catalog:read, quotes:create, orders:create, orders:read, checkout:create, and the webhook scopes it needs.

The read, redemption, and reporting steps are independent because v1 and their v2 replacements operate on the same underlying items. Treat protected headless checkout and any settlement migration as separate reviewed projects.