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.redirectUrlis the prefilled partner signup link, keyed on the order item id (og_click/click_id), and is also persisted asmetadata.handoff.redirectUrlon 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 statussubmitted. - The
order.item.createdevent payload now includesstatus(pendingorhandoff), so a webhook consumer can tell the two apart. - A
lead_genoffer with no signup template configured is rejected with422. - Providers accept or reject the item against the order once the signup
completes; commission accrues on
acceptedlike 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": trueinsidedataand anX-Offergrid-Training: trueheader, so a consumer can route practice traffic before parsing the body. Live deliveries carry"training": falseand no header. - Notification emails are prefixed
[Training]; in-app notifications are prefixedTraining:. - 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.
isTraining (or sending false) is unchanged behaviour.Contact consent on public checkouts
Changed — If you render your own checkout against these endpoints, display the
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.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
See Reseller webhooks for the full guide.
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.
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.