Skip to main content
Every change to the public API — new endpoints, new fields, corrected documentation, and any deprecation — is recorded here, newest first. See Versioning for what counts as a breaking change and how deprecations work.
This changelog starts on 2026-08-28. Changes made before that date are not listed individually; the OpenAPI specs are the authoritative record of the current surface.
Partner-signup offers become handoff items
Changed — POST /reseller/orders captures lead_gen offers as handoff itemsA partner-signup (lead_gen) offer placed through the reseller API or dashboard now creates an order item in status handoff, the same way the consumer shop has always recorded them, instead of a plain pending item. The customer finishes signup on the provider’s site:
  • The create response carries handoffs: [{ orderItemId, offerId, offerName, providerName, redirectUrl }], one per handoff item. redirectUrl is the prefilled partner signup link, keyed on the order item id (og_click / click_id), and is also persisted as metadata.handoff.redirectUrl on the item so it can be read back later.
  • Handoff items are priced and billed by the provider and are not included in totalMonthly. A handoff-only order starts in status submitted.
  • The order.item.created event payload now includes status (pending or handoff), so a webhook consumer can tell the two apart.
  • A lead_gen offer with no signup template configured is rejected with 422.
  • Providers accept or reject the item against the order once the signup completes; commission accrues on accepted like any other item.
Training orders
Added — isTraining on POST /reseller/orders, training on every order eventResellers can now mark an order as a training order at placement, from the dashboard’s Review & Submit step or with "isTraining": true on POST /reseller/orders. A training order is delivered to the provider and moves through the normal lifecycle exactly like a live one; what changes is labeling and bookkeeping:
  • Order and order-item responses carry isTraining.
  • Every webhook delivery for the order carries "training": true inside data and an X-Offergrid-Training: true header, so a consumer can route practice traffic before parsing the body. Live deliveries carry "training": false and no header.
  • Notification emails are prefixed [Training]; in-app notifications are prefixed Training:.
  • Training orders never accrue commission, never create customer records, and are excluded from dashboard analytics.
  • Resellers can delete orders from the dashboard (any order for admins, their own training orders for members; orders with paid-out commission can only be cancelled). A deleted order disappears from the provider’s list with an in-app notice and no email or webhook.
Omitting isTraining (or sending false) is unchanged behaviour.
Contact consent on public checkouts
Changed — consents is now required on POST /public/links/{slug}/orders and POST /public/shop/ordersBoth public order endpoints now require a granted order_contact consent decision in a new consents array; without it the request is rejected with 422 and code: "consent_required". This is the TCPA fix described in the Contact consent guide: the person placing the order must be shown, and agree to, a disclosure naming the providers who will contact them. Send the decision exactly as the person made it — the disclosure text is owned by the API and re-rendered server-side from disclosureVersion, so the client never sends the wording.
If you render your own checkout against these endpoints, display the order_contact.v1 text (naming every provider on the order and the phone number) next to a required checkbox, and the marketing_calls_texts.v1 text next to an optional, unchecked checkbox that never gates submit. The current wording is in the request schema description; contact us before building a custom checkout so the text you show and the version you send match.This is a breaking change under our versioning policy, made without a deprecation window because the previous behaviour — accepting a phone number for provider contact without a compliant consent record — was a live legal exposure for every provider on the platform.Added — consents on POST /public/shop/clicksOptional. When contact.phone is sent, a granted order_contact decision (handoff_contact.v1) is required for the phone to be passed to the provider; without it the phone is dropped from the handoff URL and the lead, and the redirect still succeeds.Added — POST /public/consent/revokeWithdraws consent for a phone number or email address. Backs offergrid.io/opt-out; always returns 200 and never reveals whether the identifier was known.Added — POST /provider/customers/{customerId}/consent/revokeProviders report a STOP or do-not-call request they received directly. Revokes every consent naming the provider’s team for that customer and flags them unsubscribed for that team only.Added — order.consents[] on GET /provider/orders/{itemId}The consent records naming your team: kind, whether granted, the verbatim disclosure text and version, the E.164 phone, timestamps, and revocation state.Added — webhook events order.consent_recorded and order.consent_revokedOne per consent decision per named provider. See Webhooks.
Cursor pagination and a working catalog search
Added — opt-in cursor pagination on every list endpointAll twelve provider and reseller list endpoints now accept limit (1–100) and cursor. Sending either switches the response from a bare array to { data, hasMore, nextCursor }; sending neither returns the complete array exactly as before, so no existing client changes behaviour.Cursor-based rather than offset-based: these lists are ordered newest-first, so a record created mid-pagination shifts every offset page and an offset client silently re-reads or skips records. See List responses for the full contract and a walk-the-list example.GET /provider/webhooks/{id}/deliveries and its reseller counterpart keep their 50-record cap for unpaginated callers; a paginated caller can now walk further back through the delivery log.Fixed — the catalog search filter did nothingGET /reseller/catalog?search=… was accepted, documented, and silently ignored — every request returned the unfiltered catalog. It now matches the offer name, description, and marketing headline, case-insensitively, combined with the other filters using AND.Changed — list ordering is now a total orderEvery list appends id as a final sort key. createdAt alone can tie, and a tie makes a cursor land ambiguously. Visible only when two records share a timestamp, where the order is now stable instead of arbitrary.Changed — status and kind are now typed in the specsGET /provider/orders?status= and GET /reseller/customers?kind= were hand-annotated query params; they now come from real DTOs, so the specs carry their enums and validation. An invalid value is a 400 rather than being ignored.
Reseller webhooks
Added — reseller webhooksResellers can now register webhook endpoints and receive order events for the orders their team placed, at POST /reseller/webhooks and the usual list/get/update/delete/deliveries operations alongside it.The contract is identical to the provider side — same four subscribable event types (order.created, order.item.created, order.item.status_changed, order.cancelled), same X-Offergrid-Signature scheme, same { id, type, version, data } envelope, same at-least-once delivery and delivery log. What differs is scoping: a provider receives events for items built on its own offers, a reseller for orders its own team placed.A hybrid team’s single webhook now covers both roles and receives each event exactly once, rather than twice.
This is additive. Existing provider webhooks are unaffected: no provider endpoint, payload, or subscription changes, and no provider begins receiving an event type it did not subscribe to.
See Reseller webhooks for the full guide.
Documentation and specification accuracy
Fixed — webhook signature verification docs were wrongThe provider webhooks guide described an x-offergrid-timestamp request header that the API has never sent. The timestamp is carried in the t= component of X-Offergrid-Signature. Any verifier written against the old documentation would have rejected every delivery. The verification examples are now tested against the signing implementation.The same page also corrected the webhook envelope (the event type field is type, not event; there is a version field and no top-level timestamp), the subscribable event list, and the delivery timeout (10 seconds, not 5).Fixed — reseller webhooks were documented but did not existThe reseller webhooks page described a registration flow and four event types that had no implementation at the time. The page was corrected to describe polling instead — and reseller webhooks then shipped the same day (see the entry above), so the page now documents the real endpoints.Fixed — rate limits were documented but not enforcedThe authentication guide published a burst limit, a sustained limit, and X-RateLimit-* response headers, none of which the API implements. See Rate limits for what is genuinely enforced: the two public /shop write endpoints, per IP.Changed — OpenAPI specs now document error responsesEvery operation in the published specs now declares 401, 403, and 500 alongside its documented error cases, and every 4xx/5xx response carries a schema rather than a bare description. Two new component schemas, ErrorResponse and ValidationErrorResponse, model the shapes described on the Errors page. Five operations that documented only a 404 now declare their success response as well.Removed — http://localhost:3000 from the published specsThe specs listed a local development server that a generated client or agent could select. Production is now the only server entry; local development is described in prose on the Introduction page.Added — new documentation pagesErrors, Rate limits, API conventions, Versioning, this changelog, and per-role reference indexes for the Provider, Reseller, and Public APIs.