Skip to content

feat(quotes): document the Quotes REST API and webhooks - #567

Open
toommz wants to merge 6 commits into
mainfrom
lago-1698
Open

feat(quotes): document the Quotes REST API and webhooks#567
toommz wants to merge 6 commits into
mainfrom
lago-1698

Conversation

@toommz

@toommz toommz commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Context

The Quotes feature shipped in the API (REST endpoints) but was never documented in the spec. Webhooks come from lago-api#6134. Both are now merged on main, and every payload here has been re-verified against it field by field.

Order forms and orders are deliberately out of scope; they get their own branches.

Changes

7 endpoints under a new quotes tag, all gated on the premium order_forms feature flag:

Endpoint Notes
GET /quotes filters on status[], order_type[], number[], owner_id[], external_customer_id[], from_date, to_date
GET /quotes/{lago_id} adds owners
GET /quotes/{lago_id}/versions paginated, slim versions
GET /quote_versions/{lago_id} adds content and billing_items
POST /quote_versions/{lago_id}/approve optional expires_at for the generated order form
POST /quote_versions/{lago_id}/void
POST /quote_versions/{lago_id}/clone

3 webhooks: quote.created, quote.approved, quote.voided.

Notes for review

  • billing_items is documented in full, from the JSON schemas in QuoteVersions::Validators::*::Schema. Its keys are camelCase, unlike the rest of the API, which the schema description calls out. Required-ness follows the update schema (what a persisted draft satisfies), with descriptions flagging the keys that only become mandatory at approval.
  • The signing and execution windows are bounded by the deal's own term (lago-api#6138). QuoteVersions::DealExpiration folds every plans[].payload.endDate and every wallet-credit / recurring-rule expirationAt into one boundary, and refuses a date landing on or after it, compared by date. It folded the version's end_date too until #6182 removed that column, so a one_off quote is now never bounded. The rule is described in prose rather than by its error code, matching the rest of the spec — no resource description names one.
  • share_token is intentionally omitted, per the comment in QuoteVersionSerializer.
  • Webhooks name the version the event happened to, not the quote's current version, so they use a version key rather than current_version. QuoteBaseObject was extracted to share the quote fields between the two shapes.
  • quote.voided fires for every void path, so the description points at version.void_reason to tell them apart.
  • Slim vs extended object variants mirror what the serializers actually emit per endpoint.
  • redocly lint and spectral lint pass. The 5 new array-params-plural warnings are unavoidable: the API's filter params really are singular (Array.wrap(params[:status])), and there is precedent (status[], coupon_code[], account_type[]).
  • The quotes tag has no externalDocs yet, as lago-doc has no api-reference/quotes pages.

Review fixes applied

From the OpenAPI Guardian sweep, both verified against merged main before changing anything:

  • One-off quotes are not unbounded. Applied, then superseded: #6182 removed quote_version.end_date from the fold, so a one_off quote is unbounded after all. The current wording says that; see the section below.
  • QuoteOrderTypeEnum no longer names lago_subscription_id as an ambient field. The enum is reused by OrderObject, which never returns it, so the bullet now attributes the field to the quote.

Now also covers lago-api#6182 + #6185

Both are merged — #6185 into #6182's branch, then #6182 squash-merged to main as 1b1c24a. Re-verified: the REST-visible surface at that commit is identical to the branches this was written from.

  • start_date and end_date are gone from QuoteVersionObject, replaced by billing_entity_code. #6182 drops both columns and their serializer keys; the billing items become the single source of truth for the deal term. Breaking for anyone reading those two keys, and they also leave the three quote.* webhook payloads, which embed this object.
  • billing_entity_code is resolved, not stored — the version's own entity, then the amended subscription's for a subscription_amendment, then the customer's, else null. An amendment cannot name one itself; approving a version that does is rejected.
  • The deal-expiration boundary no longer folds the version end_date — it is the earliest plan endDate and wallet / recurring-rule expirationAt. So a one_off quote is genuinely unbounded, which is why the MF-1 fix above is struck through.
  • overrides.amountCurrency on plan and coupon billing items (#6185), enum'd to the currency list. The figure is restated, not converted — 150000 USD becomes 150000 EUR.
  • payload.startDate is no longer required and has no fallback; a plan naming none starts when the order executes.
  • A concurrent write is now a 422 instead of a 500, on approve, void and clone.

QuoteVersions::UpdateService also refuses a currency change on an amendment, but that path is GraphQL-only, so it is not documented here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant