Skip to content

feat(orders): document the Orders REST API and webhooks - #569

Open
toommz wants to merge 5 commits into
lago-1699from
lago-1700
Open

feat(orders): document the Orders REST API and webhooks#569
toommz wants to merge 5 commits into
lago-1699from
lago-1700

Conversation

@toommz

@toommz toommz commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Stacked on #568 (lago-1699), itself stacked on #567. Base is lago-1699, so review only the orders commit; each PR retargets as its parent merges.

Context

Last step of the Quotes documentation stack: quote → order form → order. The order is what actually applies the deal.

Sources, all merged:

All three are now merged, and everything here has been re-verified against main.

Changes

3 endpoints under a new orders tag, gated on the premium order_forms feature flag:

Endpoint Notes
GET /orders filters on status[], order_type[], execution_mode[], customer_id[], number[], order_form_number[], quote_number[], owner_id[], search_term, and an executed_at range
GET /orders/{lago_id}
POST /orders/{lago_id}/execute optional execution_mode; synchronous

2 webhooks: order.created, order.executed.

Notes for review

  • billing_snapshot is on the REST responses but not the webhooks. #6134 makes it opt-in on OrderSerializer and passes includes: %i[billing_snapshot] from the controller only. That's the one place the two shapes diverge, so OrderObject (webhooks) and OrderObjectExtended (REST) split on exactly that field.
  • billing_snapshot reuses QuoteBillingItems from feat(quotes): document the Quotes REST API and webhooks #567 — it is literally quote_version.billing_items, so the camelCase blob is documented once for the whole stack.
  • execution_record is documented key by key, including which keys each order type fills: invoice_id for one_off, subscription_ids / applied_coupon_ids / wallet_ids for subscription_creation, plus terminated_subscription_ids for subscription_amendment. Every key is always present per EXECUTION_RECORD_DEFAULTS, which the schema description states so integrators don't code around missing keys.
  • terminated_subscription_ids is only filled when the amendment rotated immediately. A downgrade schedules the replacement and leaves the target running, so the array stays empty. Documented as such.
  • order.executed fires on success only. A failed attempt rolls back, sets failed, writes execution_record.errors, and emits nothing; Lago does not retry. Both the status enum and the webhook description say so, since "no webhook" is easy to misread as "still running".
  • execute is synchronous and idempotent (#6162). The response carries the resulting status and, on failure, the execution error, so the 422 body is where the reason lives rather than a later webhook. Re-executing an executed order returns it untouched; a failed order is retried. execution_mode in the body is only persisted when it differs from the stored value, which is what keeps a retry from tripping not_editable.
  • Automatic execution still needs execute_at. The clock job runs over Order.executable, which requires a non-null execute_at set at signing time via feat(order-forms): document the Order forms REST API and webhooks #568's mark_as_signed. Without it the order simply waits for an on-demand execute.
  • The execute endpoint can now 422 on a date the caller never sent (#6138). It only permits execution_mode, but supplying one that differs from the stored value routes through Orders::UpdateService, whose effective_execute_at falls back to the order's stored execute_at and re-validates it against the deal's term. An order with no execute_at is not bounded that way, so execution-time failure stays the backstop — both documented on the endpoint.
  • 422 also covers lock_acquisition_failed, which is a code on the error body rather than an error_details entry.
  • OrderExecutionModeEnumOrNull mirrors the Currency / CurrencyOrNull precedent, since the order's execution_mode is nullable while the mark_as_signed input's is not.
  • redocly lint and spectral lint pass with 0 errors.

One thing worth a decision

The three PRs add 18 array-params-plural warnings in total (5 + 5 + 8), on top of the 6 already on main. The filter params really are singular in the API, so the only ways to clear them are to document these filters as scalars — which is valid here, the contracts accept both, but hides that they take arrays — or to relax the rule for these paths. Happy to do either; I defaulted to documenting the more capable form.

🤖 Generated with Claude Code

Review fixes applied

From the OpenAPI Guardian sweep, verified against merged main:

  • A 404 is not only "order not found". The subscription_creation and subscription_amendment executors raise not_found_failure! for a deleted plan, charge, fixed charge, coupon, billable metric or amended subscription. record_execution_failure! has already marked the order failed, so a 404 can leave a failed order behind. Now documented.
  • The retry limit is on the mode, not the retry. The sweep claimed a non-created order is rejected outright; that is not what the code does — BaseExecuteService only short-circuits on executed?, and a request spec asserts a failed order re-executes. The real limit is that Orders::UpdateService gates on created, so a failed order can be retried but not switched to another execution_mode. Documented that way rather than as reported.

Now also covers lago-api#6182 + #6185

Both merged to main as 1b1c24a, and re-verified against it.

  • Concurrency surfaces two ways on execute, now documented. A mode change losing the race on the quote comes back as a 422 validation error via Orders::UpdateService, while the execution itself losing it keeps returning the lock code, because ExecuteOrderJob relies on that error to retry a scheduled run.
  • billing_snapshot picks up overrides.amountCurrency for free, since it reuses QuoteBillingItems from feat(quotes): document the Quotes REST API and webhooks #567.
  • OrderObject and execution_record are unchanged by both PRs.

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