Changelog
v2 is in beta. Changes that alter an existing response are listed here as beta-breaking and are kept to the minimum; everything else is additive.
Any issued item has a printable document
GET /v2/items/{code}/document.pdf streams the document of one item, whatever family it belongs to: a gift card, an event ticket, a special-offer voucher or a benefit deal. It answers 200 with application/pdf, or 404 with the JSON error envelope, so branch on the response Content-Type rather than on the status.
It takes items:read and answers to exactly the same tenancy rule as reading the item, so a benefit deal is printable by the company it was issued to as well as by the company that issued it. A foreign code, an unknown code and a document that cannot be produced all answer the same 404.
This is the per-item counterpart of the per-line download on a headless order. Use the line route when you hold a po_… order; use this one when you hold a code.
See Endpoints — One item, one document.
Special offers are sold through the API
GET /v2/products publishes a company's special offers as a fourth product family: type of special_offer, id so_<id>, one sov_<id> option per voucher. They are quoted and ordered through the same POST /v2/quotes and POST /v2/orders as everything else, and issue vouchers that read and redeem at /v2/items like any other item. Narrow the catalog to them with ?type=special_offer.
Each option publishes three prices, all whole numbers in the smallest unit of the currency: amount is what the buyer pays, regular_amount is what it cost before the offer, and redemption_value is what the issued voucher is worth when it is spent. Those last two are the ones worth reading closely. An offer configured to redeem at the regular price sells at amount and redeems at regular_amount, so a receipt that shows only what was paid will disagree with the operator's redeem screen. discount_percent is the saving the operator advertised, and available_until is the day the offer stops being orderable.
Its order_fee is charged once per line, which is what fee_basis of per_line says. A special offer cannot be posted: delivery.method of postal is refused and amounts.shipping is always 0.
Two refusals are new and specific to this family. A line that loses the last voucher to another buyer between the quote and the order answers 409 with insufficient_stock — nothing in the body is wrong, so re-read the catalog rather than correcting a field. A quantity outside the option's own bounds answers 422 with below_minimum or above_maximum on items.<n>.quantity.
Benefit deals share the same storage but are not sold: they are issued to a company and are read and redeemed by code only. They never appear in the catalog.
commerce_list_products, commerce_create_quote and commerce_create_order on the MCP server cover the family too, so an agent can discover an so_ offer and order one of its sov_ vouchers.
See Endpoints — A special offer and its vouchers.
Event tickets are sold through the API
GET /v2/products publishes a company's events as a third product family: type of event_ticket, id evt_<id>, one evto_<id> option per ticket type. They are quoted and ordered through the same two routes as a gift card, and issue tickets that read and redeem at /v2/items. Narrow the catalog to them with ?type=event_ticket.
GET /v2/events is unchanged and is still the reporting view. It reports how many tickets an event sold, held and started with, and publishes no prices, no ticket types and no id an order can name. The catalog is where tickets are bought.
Each event carries an event block: starts_at, ends_at and date_precision, the venue, whether the tickets are grouped, whether the organiser accepts a comment and what to label it, what the event requires of a buyer, its capacity mode, and its epp_<id> collection points. Each option carries its price, its ticket_fee, its availability and a requires block saying whether that ticket needs a code, an address or a collection point. An option marked add_on cannot be bought on its own.
The ticket fee is charged per seat, not per line. fee_basis of per_ticket on the product says so, and a two-seat line is charged the fee twice. Every other family stays per_line. A basket totalled without reading fee_basis will be short by a ticket fee on every seat after the first.
An event line may carry a ticket object with comment, code, collection_point_id or address — each one conditional on something the product publishes. An event cannot be posted: delivery.method of postal is refused and amounts.shipping is always 0.
Two rules differ from the Lifepeaks order form, deliberately. The catalog lists exactly what it sells, so an undated event is neither listed nor orderable, where the order form hides it and still lets a direct link buy it. And an event that counts orders rather than seats caps the tickets of the whole order, where the order form checks each line on its own.
commerce_list_products, commerce_create_quote and commerce_create_order on the MCP server cover the family too, including the ticket object, so an agent can discover an evt_ event and order seats from its evto_ ticket types.
See Endpoints — An event and its tickets.
A discount code can be applied at checkout
POST /v2/quotes and POST /v2/orders accept a discount_code — the code the buyer typed, 5 to 25 characters, matched case-insensitively against the campaigns the partner created in the Lifepeaks back office. Lifepeaks decides whether it applies and calculates the money; there is nowhere to send a discount amount.
The result comes back in three places, all of them always present so nothing has to branch on a missing key: amounts.discount as a positive figure that is subtracted, line_items[].discount as each line's share, and a top-level discount of {code, percent} or null. The code is kept on the order as it was sent, so renaming or deleting a finished campaign cannot rewrite what the order says.
What a code can reduce: value gift-card lines, experience lines, and postage. A shipping campaign comes off amounts.shipping itself and leaves amounts.discount at 0, exactly as the Lifepeaks order page reduces the postage line. Fees are never discounted, and neither is an event ticket or a special offer — an order made only of those answers 422 with discount_code_not_applicable. An unknown, switched-off, unopened, expired or exhausted code answers 422 with discount_code_not_found, one answer for all five, so the field cannot be used to discover a competitor's campaigns.
The card is still worth its full face value: the buyer pays less and redeems the same. The selling partner absorbs the gap, and the Lifepeaks commission is calculated on the discounted subtotal.
A code is consumed when the order is written, not when it is quoted, so a limited-use code can run out between the two. commerce_create_quote and commerce_create_order on the MCP server take the same field.
See Headless checkout — A discount code.
An order can be placed by a company
sender on POST /v2/orders accepts a nested company object: name and vat_number always, and street, postcode, city and country as an all-or-nothing set. It is the buyer's own company — who paid — and it is on the sender alone. The company an envelope is addressed to is still delivery.address.company_name, a different field with a different meaning.
POST /v2/quotes accepts it and ignores it, so a body a quote took is a body the order takes. Validation happens at order time.
A product may now refuse a private buyer. An experience or a special offer whose company_required is true, and an event whose event.requires.company is true, answer 422 with a new error.code of sender_company_required when the order carries no company, or carries one without its address. That flag was published before this and nothing enforced it, so read it and show the company fields before the buyer reaches payment.
One correction to earlier documentation: company_required means the buyer must be a company. It was previously described as meaning the buyer must name the company the experience is redeemed at, which was wrong.
The order reads sender.company back on the authenticated calls, with country as the alpha-2 code it was sent as. It is buyer identity, so it never appears in a webhook payload. Where the selling partner has configured business-buyer wording, an order marked as bought by a company now prints that text on the gift card — expect it to appear on the first such order.
commerce_create_order on the MCP server takes the same object.
See Headless checkout — Buying as a company.
An own-post card can choose how fast it travels
A company that posts its own cards can offer more than one speed, and until now v2 could price the faster one but never sell it. The postal entry in a product's delivery_methods now carries a priorities array of spri_<id> entries with a label, a description, a per-card price in the smallest unit of the currency, and which one is the default. POST /v2/orders and POST /v2/quotes take the buyer's pick as delivery.shipping_priority.
The key is absent whenever there is no real choice — fewer than two priorities, two that price the same, or a default that is not the cheapest. Read its presence as "offer the buyer a choice" and leave the control out otherwise. Omitting the field charges the default, which is always the cheapest and is the price destinations[] already publishes, so an order that says nothing prices exactly as it did before the field existed.
Two named refusals come with it: shipping_priority_unsupported when the company ships with a courier, which offers one speed, and shipping_priority_unavailable when the id is not one that company publishes. The chosen priority reads back on the order as delivery.shipping_priority, and is null for a courier parcel.
commerce_create_order on the MCP server takes the same field.
See Endpoints — How the card is delivered.
An order line says how many items it issued, and where its document is
Every line of an order now carries issued_count, and every line of a fulfilled order carries a family-neutral pdf block beside the gift_card_pdf it already had. Both are additive; nothing that reads gift_card_pdf has to change.
pdf and gift_card_pdf are the same link under two names. pdf is on every fulfilled line whatever it sold. gift_card_pdf stays on gift-card and experience lines forever, because integrations already read it, and is deliberately absent from event and special-offer lines where the name would be a lie. Read pdf in new code.
issued_count is how many items the line actually produced, which is not always its quantity: an event that groups its tickets issues one ticket carrying every seat, so a four-seat line reports 1. Use it to know how many codes to expect. The codes themselves still come from GET /v2/items?order_id=<reference>, and each code's own document from GET /v2/items/{code}/document.pdf.
An order can carry the buyer's marketing consent
POST /v2/orders accepts a marketing_consent object: whether the buyer opted in, where they were asked, the exact wording they were shown, and your own label for that wording. Additive — omit it and the order behaves exactly as it does today, which is also what an unticked box means.
consent_text is the evidence and is required whenever granted is true. Send the sentence that was actually on the page, verbatim. granted: false records that the buyer was asked and declined, which is worth keeping.
The consent reaches GET /v2/subscribers only after the order's payment is captured, so a buyer who consents and then abandons checkout never appears — the same rule the Lifepeaks order pages have always applied.
One thing to check on GET /v2/subscribers: the list now has two sources, so a row's id is no longer always a number. Rows collected on Lifepeaks order pages keep the numeric ids they have always had, and a consent recorded on a v2 order carries a prefix. Treat the value as opaque and send it back to starting_after unchanged. The order is stable, so a full walk still visits every subscriber exactly once.
commerce_create_order on the MCP server takes the same object, so an agent can record a consent the HTTP API accepts.
See Headless checkout.
Every list answers one envelope
Beta-breaking. Every list now answers { "object": "list", "data": [...], "has_more": <bool>, "next_cursor": <string or null> }. Until now the v2-native reads used data while GET /v2/api-keys, /v2/items, /v2/promotions, /v2/subscribers, /v2/webhook-endpoints and /v2/webhook-deliveries put their rows under items, and the event lists under events and participants. Those keys are gone, and so are itemsCount, eventsCount, participantCount and the filter echo: count data yourself, and read next_cursor for the next page on every list. GET /v2/products, /v2/companies and /v2/pickup-points gained next_cursor (always null on a pickup lookup, which never pages).
Also beta-breaking: GET /v2/items/{code} returns the item object itself, no longer a one-row list, matching GET /v2/products/{product_id}.
The two reporting routes are reports rather than lists and keep their items / itemsCount rows with offset paging.
An order can be refunded through the API
POST /v2/orders/{order_id}/refund returns money to the buyer through the gateway that took it. Send an amount for part of it, or nothing to return everything still refundable. Card refunds were admin-only before this, so a partner had to ask a person for every one.
Read refunded and refundable off the response rather than assuming the amount went through in full: a gift card used as payment absorbs part of a refund before the card is touched, and the gateway clamps. The figures come back off the payment itself.
A refund is a payment in reverse, so the same rule applies: a test key cannot refund in production.
It takes the new orders:refund scope, deliberately separate from orders:settle: a key that may settle an invoice should not thereby move money back out.
tax carries the real VAT, and an order names its seller
tax was always 0 on quotes and orders. That was never a statement about Danish VAT law, only a figure v2 did not calculate. It now carries the VAT on the order fee and the shipping fee.
No total changes. tax is already inside total and always was; what changed is that the number now describes the tax contained in it rather than reading zero. Do not add it on top. The face value of a gift card is not taxed at purchase, because a card is exchanged for goods or services later and the VAT on those belongs to that sale, so an order with no order fee and no shipping still reads 0.
The order fee and the shipping can carry different rates, so no single rate is published anywhere. A quote and the order it produces always carry the same figure.
Every order read also gained seller, with the selling company's name and vat_number — its VAT registration number, the CVR number for a Danish company. With tax and total it is everything a compliant receipt needs, without a second lookup. Additive.
An order announces itself before it is paid
The new order.created event fires when an order is accepted, before any payment. It is queued immediately after the order commits, so it never describes an order that rolled back, and a retried create fires no second event. Queueing it deliberately cannot refuse the order — a failure to write the outbox row is logged and the order still succeeds — so treat the event as a prompt and reconcile with GET /v2/orders rather than as a guarantee that one exists for every order.
Subscribe to it to open your own record the moment the order exists rather than waiting for the payment that may never come.
Orders can be listed
GET /v2/orders returns the acting company's orders, newest first, each the same full object GET /v2/orders/{order_id} returns. Narrow it by status, payment_status, created_after, created_before, email and client_reference; filters combine with AND and each may be repeated. email matches the buyer, which is the card's sender rather than its recipient.
The orders come back under data, in the same {object: "list", data: [...]} envelope GET /v2/products and GET /v2/companies answer with. It pages with limit and starting_after and answers has_more and next_cursor, with a default page of 25. It is a window over many orders, not a replacement for reading one: when you need the authoritative state of a particular order, read that order.
Every response reports the key's request budget
Every /v2 response now carries X-Rate-Limit-Limit, X-Rate-Limit-Remaining and X-Rate-Limit-Reset. The default budget is 600 requests per minute per key, it refills continuously, and each key has its own — so a client staying under the rate never sees a change. An empty budget answers 429 with error.code of rate_limited and a Retry-After, and does no work. A 429 is always safe to retry, and retrying a write with the same Idempotency-Key cannot duplicate it.
Test keys, and where they may take money
A key is lp_live_… or lp_test_…, and GET /v2/me reports which as environment. Both read and write the same data — there is one Lifepeaks database per environment — and the difference is where the key may take money.
What makes a payment real is the environment. On demo the payment credentials belong to QuickPay's test account, so every payment there is a test payment and testers pay with QuickPay's test cards. In production the credentials are live, and a test key is refused there with 409 and error.code of test_key_not_accepted_in_production — for a checkout session and for a refund alike, before anything is charged.
An endpoint can be tested before an order depends on it
POST /v2/webhook-endpoints/{endpoint_id}/ping queues a signed webhook.ping for one endpoint: the same secret, the same headers, the same retry schedule, and it appears in GET /v2/webhook-deliveries like any other delivery. If you can verify a ping, you can verify anything.
It answers 202 — the delivery is queued, and its outcome arrives later. Idempotency-Key is required and doubles as the event's identity, so a retried request cannot flood your handler. webhook.ping cannot be subscribed to; a handler that does not recognise it should answer 2xx and ignore it.
Orders nobody paid for are written off
The new order.expired event fires once for an order whose hosted checkout link lapsed more than an hour ago, or that never started checkout and is more than a day old, and the order's payment status becomes cancelled. It is terminal. An order whose card was declined expires on the same two windows, because the buyer can retry checkout on the same order and a decline is not terminal on its own.
Before this, an abandoned checkout stayed pending_payment for ever and could not be told from one still being paid. Subscribe to order.expired if you keep your own basket state.
Four more lists page by cursor
All four of those, plus GET /v2/webhook-deliveries, now answer has_more and next_cursor. Treat next_cursor as opaque: read it off a response and send it back as starting_after unchanged, and do not build, parse or compute one. On the lists that publish no next_cursor, send the id of the last row instead.
GET /v2/events/{slug}/participants answers 200 with an empty array for an event with no participants. It used to answer 404, which could not be told from an event that does not exist; a 404 now means only that.
A card can be posted, and postage is its own amount
POST /v2/orders accepts delivery.method of postal, with a delivery.address and an optional delivery.pickup_point_id. The address is checked against the destinations the product's postal method publishes for the shape of delivery asked for, so an order the courier cannot ship is refused before any money moves rather than failing days later with the payment already taken. The country is a two-letter ISO 3166-1 code.
A courier shipment cannot be scheduled. send_at together with postal answers 422 rather than being silently discarded.
An order reads back the buyer's choices, and has a number people can read
POST /v2/orders and GET /v2/orders/{order_id} now echo sender, recipient, greeting and delivery, read off the card itself. It is what a thank-you page needs and what a partner reconciling a postal order needs, without smuggling either through the payment redirect.
Both also carry reference, the short order number Lifepeaks operators search by — A7K2M9XQ4B rather than po_ and 32 hexadecimal characters. Print it on a thank-you page and quote it to support. id is still the only thing an API call addresses the order by.
The greeting message is the text the buyer wrote, or null. The caller is the company whose own storefront collected the note, and the same response already echoes both parties' e-mail addresses, so the text is not a new category of buyer detail on that surface. It is absent from every webhook payload, which carries no buyer detail at all. delivery.send_at is null on an order that was not scheduled, rather than the moment it happened to be sent, and delivery.method is how a subscriber tells a posted card from an e-mailed one after order.fulfilled fires.
Webhook payloads carry less, on purpose. They embed the same order object without sender, recipient, greeting or the postal address, because a payload goes to a URL you configured rather than to an authenticated caller. No buyer e-mail address has ever appeared in one and none starts to now. reference, delivery.method and delivery.send_at are additive there and name nobody.
A card can carry a greeting and a delivery choice
POST /v2/orders accepts two new optional objects. greeting prints a message on its own page of the gift card and shows it in the delivery e-mail, with an optional picture beside it. delivery says how the card reaches its recipient and, on the recipient's own e-mail, when.
send_at takes an RFC 3339 timestamp with an offset. One without is refused rather than read as a local time, because a card meant for Christmas morning that arrives nine hours late is a real failure. A scheduled order stays paid with fulfillment.status of processing until its moment arrives, and order.fulfilled fires then rather than at payment — plan reconciliation around that.
Nothing changes for an order that sends neither. Both are additive, and an order that omits them is delivered exactly as before.
The product says how a card is delivered, personalized and licensed
Every product also gained personalization and terms. personalization says whether a greeting is offered, how long it may be, which file types and size a buyer's own picture may be, which pictures the company offers for the greeting page as designs with gid_ ids, and the decorative amount-step strip. terms carries the company's policy link, or the policy text to show before payment, sanitised.
All of it rides on the product because it is per-company configuration, not per-product. One request builds the whole page, the list a storefront renders is the list an order is checked against, and there is no second route to keep in step.
GET /v2/pickup-points?country=DK&postcode=2200 is the one delivery read with a route of its own, because collection points open, close and move. It is a live call to the courier, and it is the one catalog route with its own rate limit: 60 lookups per company per minute, then 429 with error.code of rate_limited. It takes catalog:read, like the catalog itself.
amount_images moved from GET /v2/companies/{company_id}/brand onto the product, as personalization.amount_images. It is the same list, and it was in two places.
A buyer can put their own picture on the greeting
Three new operations under /v2/greeting-images/upload-intents reserve an upload slot, take the bytes, and check them: the same shape the gift-card PDF upload uses, so one client covers both. JPEG, PNG and WebP, up to 10 MB.
Finalize checks the declared checksum and byte size, and the file's own leading bytes. The declared type is only a claim — a file announced as image/png that does not begin with PNG's signature is refused, because what this accepts is drawn into a PDF and e-mailed.
A finished picture works exactly once. One order may name it as greeting.image.upload_id, and a second naming the same id is refused, so one buyer's photograph can never reach another buyer's card. A picture no order uses is deleted after seven days. These three take orders:create, not catalog:read: staging a buyer's picture is part of placing that buyer's order.
Experiences under a running campaign are no longer published
An experience carrying an active gift-card campaign was listed and sold by this API at its full price, while the Lifepeaks order page priced the same experience at the campaign price on the same day. The API applies no campaign, so it now withholds the product instead: such an experience is absent from GET /v2/products, answers 404 not_found by id, and its options are neither resolvable, quotable nor orderable. It returns by itself when the campaign ends, and a closed, switched-off or gift-card-value campaign never withheld it. Sell that experience through the Lifepeaks order page while its campaign runs.
quantity.maximum is enforced, not just advertised
The catalog published a maximum the order path did not apply, so an option with no configured maximum advertised 100 while a single line could ask for a thousand cards. One number now serves both: quantity.maximum is what GET /v2/products publishes and what POST /v2/quotes and POST /v2/orders enforce. An option configuring none is published and sold at 100. Publishing stock lowers it to what remains; hiding stock keeps the configured maximum and refuses above the real stock at order time. Every option is capped at 1000, the Lifepeaks order page's own per-line ceiling.
POST /v2/orders also gained an order-wide ceiling of 1000 cards across all lines, alongside the existing limit of 20 lines. Crossing it answers 422 on the items.<n>.quantity that crossed the total.
Order totals are checked against what a payment can hold
New error.code: amount_precision_unsupported, 422, on POST /v2/orders. A Lifepeaks payment amount carries at most six significant digits, so 9000.70 and 100050.00 are storable and 10024.95 is not — a rule about digits, not about size. Such a total was previously written and then failed at checkout forever, or surfaced as a 500. It is now refused before anything is written: no order, no payment, no card, no stock movement, and the idempotency key is released. The error names items. Split the basket over two orders, or pick amounts that leave no øre on the total. POST /v2/quotes is unaffected, so a basket that priced cleanly can still be refused when it is ordered.
Experiences no longer need a value gift card behind them
A company that never configured gift-card amount limits sells no variable-value card, and GET /v2/products, POST /v2/quotes and POST /v2/orders all answered 500 for it. They now work: the list holds the company's experiences alone, and a quote and an order need no value product. GET /v2/products/gift_card_value answers 404 not_found for such a company, and a value line naming it is refused by name. A company that is inactive, or that has no commerce catalog at all, now answers a typed 422 instead of a 500.
A line's gift-card PDF carries every card the line bought
GET /v2/orders/{order_id}/lines/{line_id}/gift-card.pdf returned only the first card of a line bought in quantity, and the rest had no route at all. One line is now one document holding every card of it, the way the Lifepeaks order page has always produced a bundle. There is still no per-card route and no per-card id.
The MCP commerce tools cover experiences
commerce_list_products returned the value gift card alone, so an agent could not discover an experience or its option id. It now answers through the same service GET /v2/products uses. commerce_create_quote and commerce_create_order share one line schema mirroring the HTTP contract: one to twenty lines, an optional product_option_id, amount documented as value-only, and the no-mixing, one-option-once and 1000-card rules stated where an agent reads them. commerce_create_order also declares the order body it takes rather than a free-form object.
The acting company may be named in a write body
One rule now covers every route: company is accepted in the query string wherever the acting company is honoured, and a POST or PATCH may carry the same slug as a company field in its JSON body instead, with the query string winning when both carry it. POST /v2/quotes is the exception, since its body accepts items alone. Key management and GET /v2/health honour no acting company at all.
Experiences became orderable
POST /v2/quotes and POST /v2/orders now take experience lines as well as value lines. An experience line names a gcv_<id> product and one of its gcvo_<id> options and carries no amount; the option's configured price is authoritative. One request carries a single value line or one to twenty experience lines, and mixing the two families answers 422. However many lines an order has, it is one payable amount and one checkout. See Endpoints — Quote and order lines.
Campaigns are not applied to experience lines. promotion_id and the embedded promotions array remain gift-card-value features.
Quote and order responses gained line_items. Every quote line, of either family, now states the fees it carries — the order fee, charged once per line. An order's line_items additionally carry the line's own poli_ id and "object": "line_item", and report product_option_id as null on a value line. Adding fields does not change what an existing caller already read.
New: GET /v2/orders/{order_id}/lines/{line_id}/gift-card.pdf — scope orders:read. Each line of a fulfilled order produces its own gift card, and each line of a fulfilled order carries the download for it in gift_card_pdf. It answers 200 application/pdf or the usual 404 JSON envelope, so branch on the response Content-Type. An experience carrying its own PDF template brands its own cards; the rest of the order uses the company's published v2 template.
The product catalog grew past the value gift card
GET /v2/products now carries one product per experience the company sells, next to the value gift card it always held. An experience is a gcv_<id> product of type gift_card_variant, priced by the gcvo_<id> options it carries rather than by a buyer-chosen amount. GET /v2/products/{product_id} reads either id form, and a foreign, private, de-scheduled or unknown id answers 404 not_found rather than 403.
The list gained the type and category filters and the shared limit / starting_after / has_more paging. See Endpoints — The product catalog.
The value product embeds its campaigns. gift_card_value gained a promotions array holding the open, public campaigns a buyer may pick, so a storefront can render the product and its offers in one request. Each entry's id is the same bare integer GET /v2/promotions publishes and POST /v2/items accepts as promotion_id. Two differences from the standalone list are worth reading before you use both: the embedded entry names its kind as kind and value rather than modification / reduced_price / added_value, and its amount_from / amount_to are whole numbers of the smallest currency unit where the standalone list uses decimals. Campaign codes appear on neither surface.
Route and field names settled
v2 is pre-launch, so the duplicate names were removed outright rather than deprecated. There is no alias: the old spellings answer 404 not_found.
List pagination
GET /v2/promotions, /v2/subscribers, /v2/webhook-endpoints, /v2/api-keys and /v2/companies now accept limit and starting_after, and report has_more. See Endpoints for the exact semantics.
The other four lists still return every row when called without parameters, so only callers that opt into limit see pages.
has_more on paged list responses. Every list above gained a has_more boolean next to its existing items (or data) array. Adding a field does not break a client that reads the array it already read.
Campaign order is stable. GET /v2/promotions orders by name, then by id for campaigns sharing a name, so a starting_after walk cannot skip or repeat one. GET /v2/subscribers orders by id.