askell-mcp 0.4.16 → 0.4.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,10 +173,12 @@ Typical agent workflow:
173
173
 
174
174
  ## API notes (short)
175
175
 
176
- - **v1** — legacy paths like `/customers/`, `/subscriptions/` (no `/v2` prefix)
176
+ - **v1** — legacy paths like `/customers/`, `/subscriptions/` (no `/v2` prefix). Contracts-only accounts refuse new legacy subscriptions (`400`, `code: legacy_subscriptions_disabled`). A subscription whose billing moved to a contract refuses cancel/activate/set_expiry/PATCH (`code: subscription_managed_by_contract`, follow `v2_endpoint`).
177
177
  - **v2** — current model: catalogs, quotes, checkouts, contracts, billing runs, coupons/promotion codes, fulfillment orders under `/v2/`
178
+ - **v2 contract changes** — `reference` (max 128, no commas) on create/patch/list filter. Item update `apply_at=now|period_end`; cancel a scheduled change with `POST .../scheduled-changes/{id}/cancel/`. Move the billing anchor with `POST .../change-anchor/`, not PATCH. PATCH accepts only `metadata`, `reference`, `payment_processor_override` — ignore the description's `delivery_address` / accounting fields; they are not on `V2SubscriptionContractPatch`.
179
+ - **v2 refunds** — billing-run charges are not Payments. `POST /v2/billing-runs/{id}/refund/` (full amount, no body). `202` means still `succeeded`; do not resend immediately. `POST /payments/{uuid}/refund/` is one-off only. `payment.*` may carry `billing_run_id`.
178
180
  - **v2 discounts** — catalog CRUD `/v2/coupons/` + `/v2/promotion-codes/` (coupon = definition, promotion code = what the customer types). Contract: `GET/POST /v2/subscription-contracts/{id}/discount|apply-code|remove-discount` (one active). Quotes take `promotion_code` and, for an existing buyer, `customer` (id) so combo discounts + promo restrictions apply. First-period totals already include coupon + combo; `quote.recurring_*` include combo but not the coupon (`discount.recurring_final_amount` while the coupon is active). Recurring `finalize` needs a verified payment method even when due-now is 0. Not the v1 `discount` 0–100 field.
179
- - **v2 fulfillment** — `GET /v2/fulfillment-orders/` for backfill; `POST .../{id}/fulfill/` (optional tracking body) and `POST .../{id}/cancel/` mark shipped/cancelled. Same body as `fulfillment_order.*` webhooks.
181
+ - **v2 fulfillment** — `GET /v2/fulfillment-orders/` for backfill; `POST .../{id}/fulfill/` (optional tracking body) and `POST .../{id}/cancel/` mark shipped/cancelled. Webhooks: `fulfillment_order.created`, `shipment_booked` (extra `shipment_id`), `fulfilled`, `cancelled`. Same body as `GET`.
180
182
  - Paths use **trailing slashes**
181
183
  - Prefer **v2** for new integrations; v1 remains for existing ones
182
184
  - Docs: [docs.askell.is](https://docs.askell.is/) · OpenAPI: [v1](https://askell.is/api/swagger/swagger.json) · [v2](https://askell.is/api/swagger/v2/swagger.json)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askell-mcp",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "mcpName": "io.github.Neschadin/askell-mcp",
5
5
  "description": "MCP server for the Askell payment and subscription API (Bun + stdio)",
6
6
  "author": "Neschadin Oleksandr",
@@ -67,7 +67,7 @@
67
67
  "typescript": "7.0.2"
68
68
  },
69
69
  "dependencies": {
70
- "@modelcontextprotocol/server": "2.1.0",
70
+ "@modelcontextprotocol/server": "2.2.0",
71
71
  "zod": "4.6.5"
72
72
  }
73
73
  }
@@ -431,11 +431,18 @@
431
431
  "description": "Successful operation"
432
432
  },
433
433
  "400": {
434
- "description": "Invalid status value",
434
+ "description": "Invalid request, or the account uses subscription contracts only and no longer creates legacy subscriptions (`code: legacy_subscriptions_disabled`). Create a subscription contract with `POST /api/v2/subscription-contracts/` instead. The guard error also carries `status: error` on this endpoint.",
435
435
  "content": {
436
436
  "application/json": {
437
437
  "schema": {
438
- "$ref": "#/components/schemas/inline_response_400"
438
+ "anyOf": [
439
+ {
440
+ "$ref": "#/components/schemas/inline_response_400"
441
+ },
442
+ {
443
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
444
+ }
445
+ ]
439
446
  }
440
447
  }
441
448
  }
@@ -727,11 +734,18 @@
727
734
  }
728
735
  },
729
736
  "400": {
730
- "description": "Invalid status value",
737
+ "description": "Invalid request, or the account uses subscription contracts only and no longer creates legacy subscriptions (`code: legacy_subscriptions_disabled`). Create a subscription contract with `POST /api/v2/subscription-contracts/` instead. On that refusal the customer is neither created nor updated.",
731
738
  "content": {
732
739
  "application/json": {
733
740
  "schema": {
734
- "$ref": "#/components/schemas/inline_response_400"
741
+ "anyOf": [
742
+ {
743
+ "$ref": "#/components/schemas/inline_response_400"
744
+ },
745
+ {
746
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
747
+ }
748
+ ]
735
749
  }
736
750
  }
737
751
  }
@@ -812,6 +826,25 @@
812
826
  }
813
827
  }
814
828
  }
829
+ },
830
+ "400": {
831
+ "description": "The request failed validation, or the subscription's billing is managed by a subscription contract (`code: subscription_managed_by_contract`). Manage the contract through `/api/v2/subscription-contracts/{id}/` instead.",
832
+ "content": {
833
+ "application/json": {
834
+ "schema": {
835
+ "anyOf": [
836
+ {
837
+ "type": "object",
838
+ "description": "Field validation errors, keyed by field name.",
839
+ "additionalProperties": true
840
+ },
841
+ {
842
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
843
+ }
844
+ ]
845
+ }
846
+ }
847
+ }
815
848
  }
816
849
  },
817
850
  "security": [
@@ -852,11 +885,18 @@
852
885
  }
853
886
  },
854
887
  "400": {
855
- "description": "Invalid status value",
888
+ "description": "The subscription is not active, or its billing is managed by a subscription contract (`code: subscription_managed_by_contract`, whatever the legacy state). Cancel the contract with `POST /api/v2/subscription-contracts/{id}/cancel/` instead (see `v2_endpoint`).",
856
889
  "content": {
857
890
  "application/json": {
858
891
  "schema": {
859
- "$ref": "#/components/schemas/inline_response_400"
892
+ "anyOf": [
893
+ {
894
+ "$ref": "#/components/schemas/inline_response_400"
895
+ },
896
+ {
897
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
898
+ }
899
+ ]
860
900
  }
861
901
  }
862
902
  }
@@ -900,11 +940,18 @@
900
940
  }
901
941
  },
902
942
  "400": {
903
- "description": "Invalid status value",
943
+ "description": "The subscription is already active, expired or over its plan's subscription limit, or the account no longer reactivates legacy subscriptions. Or its billing is managed by a subscription contract (`code: subscription_managed_by_contract`, whatever the legacy state): manage the contract through `/api/v2/subscription-contracts/{id}/` instead (see `v2_endpoint`).",
904
944
  "content": {
905
945
  "application/json": {
906
946
  "schema": {
907
- "$ref": "#/components/schemas/inline_response_400"
947
+ "anyOf": [
948
+ {
949
+ "$ref": "#/components/schemas/inline_response_400"
950
+ },
951
+ {
952
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
953
+ }
954
+ ]
908
955
  }
909
956
  }
910
957
  }
@@ -952,11 +999,18 @@
952
999
  }
953
1000
  },
954
1001
  "400": {
955
- "description": "Invalid status value",
1002
+ "description": "Invalid date or subscription state, or the subscription's billing is managed by a subscription contract (`code: subscription_managed_by_contract`). Manage the contract through `/api/v2/subscription-contracts/{id}/` instead.",
956
1003
  "content": {
957
1004
  "application/json": {
958
1005
  "schema": {
959
- "$ref": "#/components/schemas/inline_response_400"
1006
+ "anyOf": [
1007
+ {
1008
+ "$ref": "#/components/schemas/inline_response_400"
1009
+ },
1010
+ {
1011
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
1012
+ }
1013
+ ]
960
1014
  }
961
1015
  }
962
1016
  }
@@ -975,7 +1029,7 @@
975
1029
  "Checkout"
976
1030
  ],
977
1031
  "summary": "Create a hosted checkout card form",
978
- "description": "Creates a hosted checkout and returns a `checkout_url` that should be rendered in an iframe. The hosted page collects card details, handles 3D Secure when required, and posts completion messages to the embedding page. The resulting `token` is a reference to a payment method that can be added to a customer after checkout status becomes `tokencreated`.\n\nCreate a checkout either for a `plan` or directly for an account `payment_processor`. When using `payment_processor`, `currency` is required. To capture only the card details, set `capture_only` to `true`; this value is optional and defaults to `false`.\n\nIf supplied, `allowed_origin` must be an origin only, for example `https://merchant.example`, with no path, query string, or credentials. It controls the single parent page origin allowed to frame this checkout and receive checkout postMessage notifications. If omitted, all checkout origins configured for the account are allowed to frame the checkout.",
1032
+ "description": "Creates a hosted checkout and returns a `checkout_url` that should be rendered in an iframe. The hosted page collects card details, handles 3D Secure when required, and posts completion messages to the embedding page. The resulting `token` is a reference to a payment method that can be added to a customer after checkout status becomes `tokencreated`.\n\nCreate a checkout either for a `plan` or directly for an account `payment_processor`. When using `payment_processor`, `currency` is required. A `plan` checkout completes into a legacy subscription, so on an account that uses subscription contracts only it is refused with 400 and `code: legacy_subscriptions_disabled`; a checkout for a `payment_processor` still works there. To capture only the card details, set `capture_only` to `true`; this value is optional and defaults to `false`.\n\nIf supplied, `allowed_origin` must be an origin only, for example `https://merchant.example`, with no path, query string, or credentials. It controls the single parent page origin allowed to frame this checkout and receive checkout postMessage notifications. If omitted, all checkout origins configured for the account are allowed to frame the checkout.",
979
1033
  "requestBody": {
980
1034
  "$ref": "#/components/requestBodies/CheckoutCreate"
981
1035
  },
@@ -991,11 +1045,18 @@
991
1045
  }
992
1046
  },
993
1047
  "400": {
994
- "description": "Invalid status value",
1048
+ "description": "Invalid request, or a checkout for a `plan` on an account that uses subscription contracts only (`code: legacy_subscriptions_disabled`). Checkouts without a `plan` are not affected.",
995
1049
  "content": {
996
1050
  "application/json": {
997
1051
  "schema": {
998
- "$ref": "#/components/schemas/inline_response_400"
1052
+ "anyOf": [
1053
+ {
1054
+ "$ref": "#/components/schemas/inline_response_400"
1055
+ },
1056
+ {
1057
+ "$ref": "#/components/schemas/LegacySubscriptionGuardError"
1058
+ }
1059
+ ]
999
1060
  }
1000
1061
  }
1001
1062
  }
@@ -1359,7 +1420,7 @@
1359
1420
  }
1360
1421
  ],
1361
1422
  "summary": "Refund a settled Payment",
1362
- "description": "If a Payment is in the settled state and the payment processor supports refunds, you can refund it. Requires a secret key.",
1423
+ "description": "If a Payment is in the settled state and the payment processor supports refunds, you can refund it. Requires a secret key. Charges made by V2 billing runs are not Payments: the transaction uuid a V2 `payment.*` webhook carries is answered with a 400 whose body names the run (`billing_run_id`) and points to `POST /api/v2/billing-runs/{billingRunId}/refund/`, which refunds it.",
1363
1424
  "responses": {
1364
1425
  "200": {
1365
1426
  "description": "Successful operation",
@@ -2590,6 +2651,47 @@
2590
2651
  }
2591
2652
  }
2592
2653
  },
2654
+ "LegacySubscriptionGuardError": {
2655
+ "type": "object",
2656
+ "description": "Returned with HTTP 400 when the legacy API refuses work that subscription contracts now own. Match on `code` rather than on the message.",
2657
+ "required": [
2658
+ "error",
2659
+ "code",
2660
+ "v2_endpoint"
2661
+ ],
2662
+ "properties": {
2663
+ "error": {
2664
+ "type": "string",
2665
+ "description": "What was refused and what to use instead, in English.",
2666
+ "example": "This account uses subscription contracts and no longer creates legacy subscriptions. Use POST /api/v2/subscription-contracts/ instead."
2667
+ },
2668
+ "code": {
2669
+ "type": "string",
2670
+ "enum": [
2671
+ "legacy_subscriptions_disabled",
2672
+ "subscription_managed_by_contract"
2673
+ ],
2674
+ "description": "`legacy_subscriptions_disabled`: the account's subscription model is `contracts`, so legacy subscriptions are no longer created. `subscription_managed_by_contract`: the subscription was migrated to a subscription contract (its `billing_managed_by` is `subscription_contract`), which must be cancelled or changed through the V2 API instead.",
2675
+ "example": "legacy_subscriptions_disabled"
2676
+ },
2677
+ "migrated_to_contract_id": {
2678
+ "type": "integer",
2679
+ "nullable": true,
2680
+ "description": "The subscription contract that bills the subscription. Only present with `subscription_managed_by_contract`.",
2681
+ "example": 123
2682
+ },
2683
+ "v2_endpoint": {
2684
+ "type": "string",
2685
+ "description": "The V2 endpoint to use instead: `/api/v2/subscription-contracts/` to create, `/api/v2/subscription-contracts/{id}/cancel/` to cancel, `/api/v2/subscription-contracts/{id}/` for other changes.",
2686
+ "example": "/api/v2/subscription-contracts/"
2687
+ },
2688
+ "status": {
2689
+ "type": "string",
2690
+ "description": "Only on `POST /customers/{customerReference}/subscriptions/add/`, matching that endpoint's other errors.",
2691
+ "example": "error"
2692
+ }
2693
+ }
2694
+ },
2593
2695
  "inline_response_400": {
2594
2696
  "type": "object",
2595
2697
  "properties": {
@@ -619,6 +619,14 @@
619
619
  },
620
620
  "description": "Filter by customer reference."
621
621
  },
622
+ {
623
+ "in": "query",
624
+ "name": "reference",
625
+ "schema": {
626
+ "type": "string"
627
+ },
628
+ "description": "Filter by contract reference (exact match)."
629
+ },
622
630
  {
623
631
  "$ref": "#/components/parameters/PageSizeFilter"
624
632
  }
@@ -699,7 +707,7 @@
699
707
  "V2 Subscription Contracts"
700
708
  ],
701
709
  "summary": "Partially update a V2 subscription contract",
702
- "description": "Restricted patch endpoint. Only `metadata` and `payment_processor_override` may be updated. Requires a secret key.",
710
+ "description": "Restricted patch endpoint. Only `metadata`, `reference`, `payment_processor_override`, `delivery_address`, `accounting_department` and `accounting_cost_center` may be updated. Requires a secret key.",
703
711
  "parameters": [
704
712
  {
705
713
  "$ref": "#/components/parameters/V2ContractId"
@@ -739,7 +747,7 @@
739
747
  "V2 Subscription Contracts"
740
748
  ],
741
749
  "summary": "Preview a V2 subscription contract proration",
742
- "description": "Previews the proration impact of updating an existing V2 subscription contract item. Requires a secret key.",
750
+ "description": "Previews the proration impact of updating an existing V2 subscription contract item. With `operation=update_item`, `apply_at` selects when the change takes effect: `now` (default) prorates from `effective_at`, which must not be in the future (a future value returns a 400 with code `future_effective_at_not_supported`); `period_end` previews a change scheduled for the item's next renewal (nothing due now, no lines, `next_billing_estimate` at the new values, and `scheduled_for`). An immediate change to a price with a different billing interval credits the unused current period, charges a full new period from `effective_at` and restarts the item's billing period (single-item contracts only, settled with `invoice_now`). update_item previews also return `next_billing_at_after_change` and `billing_cadence_after_change`. Requires a secret key.",
743
751
  "parameters": [
744
752
  {
745
753
  "$ref": "#/components/parameters/V2ContractId"
@@ -786,7 +794,7 @@
786
794
  "V2 Subscription Contracts"
787
795
  ],
788
796
  "summary": "Update a V2 subscription contract item with proration",
789
- "description": "Executes a prorated update for an existing V2 subscription contract item. Requires a secret key.",
797
+ "description": "Updates an existing V2 subscription contract item. With `apply_at=now` (default) the change applies immediately and `effective_at` only positions the proration window, so it must not be in the future (a future value returns a 400 with code `future_effective_at_not_supported`). With `apply_at=period_end` nothing is changed or charged now: the change is stored as a scheduled change (returned in `scheduled_change`, with `change_id` null) and applied to the item when its next renewal billing run is created. `effective_at`, `settlement_behavior` and `include_pending_adjustments` cannot be combined with `period_end`. An item with a pending scheduled change rejects further updates with a 409 (`scheduled_change_exists`) until it is canceled. An immediate billing-interval change with an amount due only switches the item once its proration run is collected (`applies_on_payment: true`, item `pending_interval_change`); meanwhile other changes to the item return a 409 (`pending_interval_change`). Requires a secret key.",
790
798
  "parameters": [
791
799
  {
792
800
  "$ref": "#/components/parameters/V2ContractId"
@@ -818,6 +826,70 @@
818
826
  },
819
827
  "404": {
820
828
  "$ref": "#/components/responses/V2NotFound"
829
+ },
830
+ "409": {
831
+ "$ref": "#/components/responses/V2ProrationConflict"
832
+ }
833
+ },
834
+ "security": [
835
+ {
836
+ "Secret-Api-Key": []
837
+ }
838
+ ]
839
+ }
840
+ },
841
+ "/v2/subscription-contracts/{contractId}/scheduled-changes/{scheduledChangeId}/cancel/": {
842
+ "post": {
843
+ "tags": [
844
+ "V2 Subscription Contracts"
845
+ ],
846
+ "summary": "Cancel a scheduled V2 subscription contract item change",
847
+ "description": "Cancels an item change scheduled with `apply_at=period_end` that has not been applied yet; the item then renews at its current values. Canceling an already canceled change replays (`replayed: true`); an applied or failed change returns a 409 with code `scheduled_change_not_cancelable`. Requires a secret key.",
848
+ "parameters": [
849
+ {
850
+ "$ref": "#/components/parameters/V2ContractId"
851
+ },
852
+ {
853
+ "name": "scheduledChangeId",
854
+ "in": "path",
855
+ "required": true,
856
+ "schema": {
857
+ "type": "integer"
858
+ }
859
+ }
860
+ ],
861
+ "requestBody": {
862
+ "content": {
863
+ "application/json": {
864
+ "schema": {
865
+ "type": "object",
866
+ "properties": {
867
+ "reason": {
868
+ "type": "string",
869
+ "nullable": true
870
+ }
871
+ }
872
+ }
873
+ }
874
+ },
875
+ "required": false
876
+ },
877
+ "responses": {
878
+ "200": {
879
+ "description": "Successful operation",
880
+ "content": {
881
+ "application/json": {
882
+ "schema": {
883
+ "$ref": "#/components/schemas/V2SubscriptionContractScheduledChangeCancelResponse"
884
+ }
885
+ }
886
+ }
887
+ },
888
+ "404": {
889
+ "$ref": "#/components/responses/V2NotFound"
890
+ },
891
+ "409": {
892
+ "$ref": "#/components/responses/V2ProrationConflict"
821
893
  }
822
894
  },
823
895
  "security": [
@@ -865,6 +937,9 @@
865
937
  },
866
938
  "404": {
867
939
  "$ref": "#/components/responses/V2NotFound"
940
+ },
941
+ "409": {
942
+ "$ref": "#/components/responses/V2ProrationConflict"
868
943
  }
869
944
  },
870
945
  "security": [
@@ -912,6 +987,59 @@
912
987
  },
913
988
  "404": {
914
989
  "$ref": "#/components/responses/V2NotFound"
990
+ },
991
+ "409": {
992
+ "$ref": "#/components/responses/V2ProrationConflict"
993
+ }
994
+ },
995
+ "security": [
996
+ {
997
+ "Secret-Api-Key": []
998
+ }
999
+ ]
1000
+ }
1001
+ },
1002
+ "/v2/subscription-contracts/{contractId}/change-anchor/": {
1003
+ "post": {
1004
+ "tags": [
1005
+ "V2 Subscription Contracts"
1006
+ ],
1007
+ "summary": "Change the billing anchor of a V2 subscription contract with proration",
1008
+ "description": "Moves the next renewal of an active V2 subscription contract, and of all its active items, to new_billing_anchor_at. With proration, the unused part of the paid period is credited and the stretch from effective_at to the new anchor is charged, on the next invoice at the new date (next_invoice) or in a proration billing run now (invoice_now). Does not extend service entitlements. While an immediate billing-interval change on an item awaits its payment (item `pending_interval_change`) it returns a 409 (`pending_interval_change`); an interval change whose payment failed is abandoned, so a later retry of its charge no longer applies it. Requires a secret key.",
1009
+ "parameters": [
1010
+ {
1011
+ "$ref": "#/components/parameters/V2ContractId"
1012
+ }
1013
+ ],
1014
+ "requestBody": {
1015
+ "content": {
1016
+ "application/json": {
1017
+ "schema": {
1018
+ "$ref": "#/components/schemas/V2SubscriptionContractChangeAnchor"
1019
+ }
1020
+ }
1021
+ },
1022
+ "required": true
1023
+ },
1024
+ "responses": {
1025
+ "200": {
1026
+ "description": "Successful operation",
1027
+ "content": {
1028
+ "application/json": {
1029
+ "schema": {
1030
+ "$ref": "#/components/schemas/V2SubscriptionContractItemUpdateResponse"
1031
+ }
1032
+ }
1033
+ }
1034
+ },
1035
+ "400": {
1036
+ "$ref": "#/components/responses/V2ValidationError"
1037
+ },
1038
+ "404": {
1039
+ "$ref": "#/components/responses/V2NotFound"
1040
+ },
1041
+ "409": {
1042
+ "$ref": "#/components/responses/V2ProrationConflict"
915
1043
  }
916
1044
  },
917
1045
  "security": [
@@ -1681,6 +1809,39 @@
1681
1809
  ]
1682
1810
  }
1683
1811
  },
1812
+ "/v2/billing-runs/{billingRunId}/refund/": {
1813
+ "post": {
1814
+ "tags": [
1815
+ "V2 Billing Runs"
1816
+ ],
1817
+ "summary": "Refund a V2 billing run",
1818
+ "description": "Refunds the charge a succeeded V2 billing run settled. Full refunds only: the whole settled amount of the run's transaction is returned to the payment method it was charged to; partial refunds are not supported. Only runs in the `succeeded` state with a settled transaction can be refunded, and the payment processor must support refunds. No request body. Requires a secret key (public keys and legacy tokens are rejected).\n\n**200** — the processor refunded the charge and the returned run is `refunded` (with the refund recorded under `metadata.transaction_refund`); a `billing_run.changed` webhook is sent.\n\n**202** — the refund is not reflected on the returned run yet, which is still `succeeded`. Either the processor accepted the refund but has not completed it (for example Teya answering PENDING), or the processor refunded the charge and the run is still being updated. The request is recorded under `metadata.refund_requests`. Wait for the `billing_run.changed` webhook, or re-read the run, to see it move to `refunded`. Do not treat a 202 as a failure and do not immediately resend the request.\n\n**400** — the run is not `succeeded`, has no settled transaction (for example a zero-amount run, or a charge already refunded), its payment processor does not support refunds, or the processor did not complete the refund.\n\nConcurrent refund requests for the same run are serialized, and a request made after an earlier one refunded the charge gets a 400 while the run is `refunded`. If a request gets no response (for example a timeout), read the run with `GET /api/v2/billing-runs/{billingRunId}/` and check its `state` and `metadata.refund_requests` before sending the request again.\n\nThe `billing_run_id` in `payment.*` webhook payloads identifies the run to refund.",
1819
+ "parameters": [
1820
+ {
1821
+ "$ref": "#/components/parameters/V2BillingRunId"
1822
+ }
1823
+ ],
1824
+ "responses": {
1825
+ "200": {
1826
+ "$ref": "#/components/responses/V2BillingRun"
1827
+ },
1828
+ "202": {
1829
+ "$ref": "#/components/responses/V2BillingRunRefundPending"
1830
+ },
1831
+ "400": {
1832
+ "$ref": "#/components/responses/V2ValidationError"
1833
+ },
1834
+ "404": {
1835
+ "$ref": "#/components/responses/V2NotFound"
1836
+ }
1837
+ },
1838
+ "security": [
1839
+ {
1840
+ "Secret-Api-Key": []
1841
+ }
1842
+ ]
1843
+ }
1844
+ },
1684
1845
  "/v2/fulfillment-orders/": {
1685
1846
  "get": {
1686
1847
  "tags": [
@@ -3011,6 +3172,12 @@
3011
3172
  "metadata": {
3012
3173
  "$ref": "#/components/schemas/V2Metadata"
3013
3174
  },
3175
+ "reference": {
3176
+ "type": "string",
3177
+ "nullable": true,
3178
+ "maxLength": 128,
3179
+ "description": "The contract's reference in an external system, such as the seller's order number. Must not contain commas. Surrounding whitespace is trimmed and a blank value means no reference."
3180
+ },
3014
3181
  "initial_billing_mode": {
3015
3182
  "type": "string",
3016
3183
  "enum": [
@@ -3029,7 +3196,18 @@
3029
3196
  "type": "object",
3030
3197
  "properties": {
3031
3198
  "metadata": {
3032
- "$ref": "#/components/schemas/V2Metadata"
3199
+ "allOf": [
3200
+ {
3201
+ "$ref": "#/components/schemas/V2Metadata"
3202
+ }
3203
+ ],
3204
+ "description": "Replaces the integration's metadata keys. Keys Askell uses itself (`askell_source`, `billing_anchor_mode`, `activation_failure`, `email_markers`, `copied_legacy_pauses`, `copied_legacy_extra_data`, `migration_source`, `migration_cadence_mode`, `legacy_subscription_ids`, `seed`) keep their current values on the contract; any of them in the request are ignored."
3205
+ },
3206
+ "reference": {
3207
+ "type": "string",
3208
+ "nullable": true,
3209
+ "maxLength": 128,
3210
+ "description": "The contract's reference in an external system. Must not contain commas. `null` or a blank value clears it."
3033
3211
  },
3034
3212
  "payment_processor_override": {
3035
3213
  "type": "integer",
@@ -3288,7 +3466,8 @@
3288
3466
  "pause",
3289
3467
  "resume",
3290
3468
  "cancel",
3291
- "restart"
3469
+ "restart",
3470
+ "change_anchor"
3292
3471
  ],
3293
3472
  "default": "update_item"
3294
3473
  },
@@ -3335,6 +3514,21 @@
3335
3514
  "format": "date-time",
3336
3515
  "nullable": true,
3337
3516
  "description": "Only applies to pause."
3517
+ },
3518
+ "new_billing_anchor_at": {
3519
+ "type": "string",
3520
+ "format": "date-time",
3521
+ "nullable": true,
3522
+ "description": "Only applies to change_anchor, where it is required."
3523
+ },
3524
+ "apply_at": {
3525
+ "type": "string",
3526
+ "enum": [
3527
+ "now",
3528
+ "period_end"
3529
+ ],
3530
+ "default": "now",
3531
+ "description": "Only applies to update_item. `now` changes the item immediately with proration; `period_end` schedules the change for the item's next renewal (cannot be combined with `effective_at`, `settlement_behavior` or `include_pending_adjustments`)."
3338
3532
  }
3339
3533
  }
3340
3534
  }
@@ -3419,6 +3613,49 @@
3419
3613
  }
3420
3614
  ]
3421
3615
  },
3616
+ "V2SubscriptionContractChangeAnchor": {
3617
+ "allOf": [
3618
+ {
3619
+ "$ref": "#/components/schemas/V2SubscriptionContractChangeCommon"
3620
+ },
3621
+ {
3622
+ "type": "object",
3623
+ "required": [
3624
+ "new_billing_anchor_at"
3625
+ ],
3626
+ "properties": {
3627
+ "operation": {
3628
+ "type": "string",
3629
+ "enum": [
3630
+ "change_anchor"
3631
+ ],
3632
+ "default": "change_anchor"
3633
+ },
3634
+ "new_billing_anchor_at": {
3635
+ "type": "string",
3636
+ "format": "date-time",
3637
+ "description": "The contract's new next renewal time. Must be after effective_at and, with proration, no more than one billing period after it."
3638
+ },
3639
+ "settlement_behavior": {
3640
+ "type": "string",
3641
+ "enum": [
3642
+ "next_invoice",
3643
+ "invoice_now"
3644
+ ],
3645
+ "nullable": true,
3646
+ "description": "Defaults to invoice_now with always_invoice proration, otherwise next_invoice. Ignored when proration_behavior is none."
3647
+ },
3648
+ "idempotency_key": {
3649
+ "type": "string",
3650
+ "nullable": true
3651
+ },
3652
+ "preview_token": {
3653
+ "type": "string"
3654
+ }
3655
+ }
3656
+ }
3657
+ ]
3658
+ },
3422
3659
  "V2SubscriptionContractItemUpdate": {
3423
3660
  "allOf": [
3424
3661
  {
@@ -3459,6 +3696,15 @@
3459
3696
  "nullable": true,
3460
3697
  "description": "Fixed per-unit amount for the item, used instead of the catalog price. Omitting the field keeps the item's current override; an explicit null clears it so the item reverts to the catalog price."
3461
3698
  },
3699
+ "apply_at": {
3700
+ "type": "string",
3701
+ "enum": [
3702
+ "now",
3703
+ "period_end"
3704
+ ],
3705
+ "default": "now",
3706
+ "description": "`now` changes the item immediately with proration; `period_end` keeps the current price until the item's current service period ends and applies the change at the next renewal, with no refund or charge now."
3707
+ },
3462
3708
  "idempotency_key": {
3463
3709
  "type": "string",
3464
3710
  "nullable": true
@@ -3613,6 +3859,155 @@
3613
3859
  },
3614
3860
  "preview_token": {
3615
3861
  "type": "string"
3862
+ },
3863
+ "apply_at": {
3864
+ "type": "string",
3865
+ "enum": [
3866
+ "now",
3867
+ "period_end"
3868
+ ],
3869
+ "description": "update_item only."
3870
+ },
3871
+ "scheduled_for": {
3872
+ "type": "string",
3873
+ "format": "date-time",
3874
+ "nullable": true,
3875
+ "description": "update_item only. When a period_end change takes effect (the item's current service period end); null for now."
3876
+ },
3877
+ "next_billing_at_after_change": {
3878
+ "type": "string",
3879
+ "format": "date-time",
3880
+ "nullable": true,
3881
+ "description": "update_item only. The item's next billing once the change is in effect. For an immediate billing-interval change this is the end of the new period; for period_end it is the renewal the change applies at."
3882
+ },
3883
+ "billing_cadence_after_change": {
3884
+ "allOf": [
3885
+ {
3886
+ "$ref": "#/components/schemas/V2SubscriptionContractBillingCadence"
3887
+ }
3888
+ ],
3889
+ "nullable": true,
3890
+ "description": "update_item only. The contract's billing cadence after the change."
3891
+ },
3892
+ "applies_on_payment": {
3893
+ "type": "boolean",
3894
+ "description": "update_item only. True for an immediate billing-interval change with an amount due: the item switches only once that charge is collected."
3895
+ }
3896
+ }
3897
+ },
3898
+ "V2SubscriptionContractScheduledChange": {
3899
+ "type": "object",
3900
+ "properties": {
3901
+ "id": {
3902
+ "type": "integer"
3903
+ },
3904
+ "contract_item": {
3905
+ "type": "integer"
3906
+ },
3907
+ "operation": {
3908
+ "type": "string",
3909
+ "enum": [
3910
+ "update_item"
3911
+ ]
3912
+ },
3913
+ "state": {
3914
+ "type": "string",
3915
+ "enum": [
3916
+ "scheduled",
3917
+ "applied",
3918
+ "canceled",
3919
+ "failed"
3920
+ ]
3921
+ },
3922
+ "scheduled_for": {
3923
+ "type": "string",
3924
+ "format": "date-time"
3925
+ },
3926
+ "price_id": {
3927
+ "type": "integer"
3928
+ },
3929
+ "price": {
3930
+ "$ref": "#/components/schemas/V2CatalogPrice"
3931
+ },
3932
+ "product": {
3933
+ "type": "object",
3934
+ "properties": {
3935
+ "id": {
3936
+ "type": "integer"
3937
+ },
3938
+ "name": {
3939
+ "type": "string"
3940
+ },
3941
+ "reference": {
3942
+ "type": "string",
3943
+ "nullable": true
3944
+ }
3945
+ }
3946
+ },
3947
+ "quantity": {
3948
+ "type": "integer"
3949
+ },
3950
+ "discount_percent": {
3951
+ "type": "string",
3952
+ "format": "decimal",
3953
+ "nullable": true
3954
+ },
3955
+ "unit_amount_override": {
3956
+ "type": "string",
3957
+ "format": "decimal",
3958
+ "nullable": true
3959
+ },
3960
+ "from_price_id": {
3961
+ "type": "integer",
3962
+ "nullable": true
3963
+ },
3964
+ "from_quantity": {
3965
+ "type": "integer",
3966
+ "nullable": true
3967
+ },
3968
+ "reason": {
3969
+ "type": "string",
3970
+ "nullable": true
3971
+ },
3972
+ "applied_at": {
3973
+ "type": "string",
3974
+ "format": "date-time",
3975
+ "nullable": true
3976
+ },
3977
+ "canceled_at": {
3978
+ "type": "string",
3979
+ "format": "date-time",
3980
+ "nullable": true
3981
+ },
3982
+ "applied_change_id": {
3983
+ "type": "integer",
3984
+ "nullable": true
3985
+ },
3986
+ "billing_run_id": {
3987
+ "type": "integer",
3988
+ "nullable": true
3989
+ },
3990
+ "created_at": {
3991
+ "type": "string",
3992
+ "format": "date-time"
3993
+ },
3994
+ "updated_at": {
3995
+ "type": "string",
3996
+ "format": "date-time"
3997
+ }
3998
+ }
3999
+ },
4000
+ "V2SubscriptionContractScheduledChangeCancelResponse": {
4001
+ "type": "object",
4002
+ "properties": {
4003
+ "scheduled_change": {
4004
+ "$ref": "#/components/schemas/V2SubscriptionContractScheduledChange"
4005
+ },
4006
+ "replayed": {
4007
+ "type": "boolean"
4008
+ },
4009
+ "contract": {
4010
+ "$ref": "#/components/schemas/V2SubscriptionContract"
3616
4011
  }
3617
4012
  }
3618
4013
  },
@@ -3620,7 +4015,21 @@
3620
4015
  "type": "object",
3621
4016
  "properties": {
3622
4017
  "change_id": {
3623
- "type": "integer"
4018
+ "type": "integer",
4019
+ "nullable": true,
4020
+ "description": "Null when the change was scheduled with apply_at=period_end."
4021
+ },
4022
+ "scheduled_change": {
4023
+ "allOf": [
4024
+ {
4025
+ "$ref": "#/components/schemas/V2SubscriptionContractScheduledChange"
4026
+ }
4027
+ ],
4028
+ "nullable": true
4029
+ },
4030
+ "applies_on_payment": {
4031
+ "type": "boolean",
4032
+ "description": "True when an immediate billing-interval change waits for `billing_run_id` to be collected; until then the item keeps its current price and shows the change in `pending_interval_change`."
3624
4033
  },
3625
4034
  "billing_run_id": {
3626
4035
  "type": "integer",
@@ -3632,6 +4041,13 @@
3632
4041
  "type": "integer"
3633
4042
  }
3634
4043
  },
4044
+ "consumed_pending_adjustment_ids": {
4045
+ "type": "array",
4046
+ "items": {
4047
+ "type": "integer"
4048
+ },
4049
+ "description": "Earlier pending adjustment lines this change pulled into its invoice_now billing run with include_pending_adjustments; empty otherwise."
4050
+ },
3635
4051
  "replayed": {
3636
4052
  "type": "boolean"
3637
4053
  },
@@ -3668,7 +4084,12 @@
3668
4084
  "type": "string"
3669
4085
  },
3670
4086
  "metadata": {
3671
- "$ref": "#/components/schemas/V2Metadata"
4087
+ "allOf": [
4088
+ {
4089
+ "$ref": "#/components/schemas/V2Metadata"
4090
+ }
4091
+ ],
4092
+ "description": "Stored on the checkout and copied to the created contract's `metadata` when the checkout is finalized. In a checkout session the session's own metadata wins on a shared key, and the sales channel's `metadata_policy.frontend_allowed_keys` limits the keys the browser may send (with no such list any key is accepted, and an empty list accepts none). Keys Askell uses itself (`askell_source`, `billing_anchor_mode`, `activation_failure`, `email_markers`, `copied_legacy_pauses`, `copied_legacy_extra_data`, `migration_source`, `migration_cadence_mode`, `legacy_subscription_ids`, `seed`) are not copied."
3672
4093
  },
3673
4094
  "delivery_address": {
3674
4095
  "$ref": "#/components/schemas/V2Address",
@@ -4001,7 +4422,12 @@
4001
4422
  "description": "Seller customer reference. If an existing customer has this reference, the session is bound to that customer."
4002
4423
  },
4003
4424
  "metadata": {
4004
- "$ref": "#/components/schemas/V2Metadata"
4425
+ "allOf": [
4426
+ {
4427
+ "$ref": "#/components/schemas/V2Metadata"
4428
+ }
4429
+ ],
4430
+ "description": "Server-side session metadata. Returned by the session GET and copied to the created contract's `metadata` when the session's checkout is finalized; on a key the browser also sends, this value wins. Keys Askell uses itself (`askell_source`, `billing_anchor_mode`, `activation_failure`, `email_markers`, `copied_legacy_pauses`, `copied_legacy_extra_data`, `migration_source`, `migration_cadence_mode`, `legacy_subscription_ids`, `seed`) are not copied."
4005
4431
  },
4006
4432
  "theme": {
4007
4433
  "$ref": "#/components/schemas/V2Metadata",
@@ -4902,6 +5328,59 @@
4902
5328
  },
4903
5329
  "metadata": {
4904
5330
  "$ref": "#/components/schemas/V2Metadata"
5331
+ },
5332
+ "scheduled_change": {
5333
+ "allOf": [
5334
+ {
5335
+ "$ref": "#/components/schemas/V2SubscriptionContractScheduledChange"
5336
+ }
5337
+ ],
5338
+ "nullable": true,
5339
+ "description": "The item's pending change scheduled for its next renewal, if any."
5340
+ },
5341
+ "pending_interval_change": {
5342
+ "type": "object",
5343
+ "nullable": true,
5344
+ "description": "An immediate billing-interval change waiting for its charge to be collected; the item switches when the billing run succeeds.",
5345
+ "properties": {
5346
+ "change_id": {
5347
+ "type": "integer"
5348
+ },
5349
+ "billing_run_id": {
5350
+ "type": "integer",
5351
+ "nullable": true
5352
+ },
5353
+ "status": {
5354
+ "type": "string",
5355
+ "enum": [
5356
+ "awaiting_payment"
5357
+ ]
5358
+ },
5359
+ "price_id": {
5360
+ "type": "integer"
5361
+ },
5362
+ "quantity": {
5363
+ "type": "integer"
5364
+ },
5365
+ "discount_percent": {
5366
+ "type": "string",
5367
+ "format": "decimal",
5368
+ "nullable": true
5369
+ },
5370
+ "unit_amount_override": {
5371
+ "type": "string",
5372
+ "format": "decimal",
5373
+ "nullable": true
5374
+ },
5375
+ "effective_at": {
5376
+ "type": "string",
5377
+ "format": "date-time"
5378
+ },
5379
+ "next_billing_at": {
5380
+ "type": "string",
5381
+ "format": "date-time"
5382
+ }
5383
+ }
4905
5384
  }
4906
5385
  }
4907
5386
  },
@@ -5478,7 +5957,8 @@
5478
5957
  "type": "string"
5479
5958
  },
5480
5959
  "customer_name": {
5481
- "type": "string"
5960
+ "type": "string",
5961
+ "description": "The customer's first and last name, whichever are set; empty when the customer has neither."
5482
5962
  },
5483
5963
  "customer_email": {
5484
5964
  "type": "string",
@@ -5758,6 +6238,12 @@
5758
6238
  "customer_reference": {
5759
6239
  "type": "string"
5760
6240
  },
6241
+ "reference": {
6242
+ "type": "string",
6243
+ "nullable": true,
6244
+ "maxLength": 128,
6245
+ "description": "The contract's reference in an external system, such as the seller's order number. V2 payment pages set it from their `subscription_reference` URL parameter."
6246
+ },
5761
6247
  "shipping_selection": {
5762
6248
  "$ref": "#/components/schemas/V2ShippingSelection"
5763
6249
  },
@@ -5883,7 +6369,12 @@
5883
6369
  }
5884
6370
  },
5885
6371
  "metadata": {
5886
- "$ref": "#/components/schemas/V2Metadata"
6372
+ "allOf": [
6373
+ {
6374
+ "$ref": "#/components/schemas/V2Metadata"
6375
+ }
6376
+ ],
6377
+ "description": "Integration metadata. A contract created by a checkout carries the checkout session's and the checkout's metadata, and `askell_source` records where the contract came from (`type`: `checkout_session`, `checkout` or `payment_page`, with the ids of that source)."
5887
6378
  },
5888
6379
  "subscriber_page": {
5889
6380
  "type": "string",
@@ -5957,6 +6448,13 @@
5957
6448
  },
5958
6449
  "latest_billing_run": {
5959
6450
  "$ref": "#/components/schemas/V2BillingRunSummary"
6451
+ },
6452
+ "scheduled_changes": {
6453
+ "type": "array",
6454
+ "description": "Pending item changes scheduled for the items' next renewals.",
6455
+ "items": {
6456
+ "$ref": "#/components/schemas/V2SubscriptionContractScheduledChange"
6457
+ }
5960
6458
  }
5961
6459
  }
5962
6460
  },
@@ -7076,6 +7574,34 @@
7076
7574
  }
7077
7575
  }
7078
7576
  },
7577
+ "V2ProrationConflict": {
7578
+ "description": "Conflict, e.g. `scheduled_change_exists`, `pending_interval_change`, `scheduled_change_not_cancelable`, `idempotency_key_conflict`, `preview_stale`, `billing_run_overlap`",
7579
+ "content": {
7580
+ "application/json": {
7581
+ "schema": {
7582
+ "type": "object",
7583
+ "properties": {
7584
+ "error": {
7585
+ "type": "string"
7586
+ },
7587
+ "code": {
7588
+ "type": "string"
7589
+ }
7590
+ }
7591
+ }
7592
+ }
7593
+ }
7594
+ },
7595
+ "V2BillingRunRefundPending": {
7596
+ "description": "The refund is not reflected on the returned billing run yet, which is still `succeeded`: either the payment processor accepted the refund but has not completed it, or it refunded the charge and the run is still being updated. The request is recorded under `metadata.refund_requests`. Wait for the `billing_run.changed` webhook, or re-read the run, to see it move to `refunded`; do not treat this as a failure or immediately resend the request.",
7597
+ "content": {
7598
+ "application/json": {
7599
+ "schema": {
7600
+ "$ref": "#/components/schemas/V2BillingRun"
7601
+ }
7602
+ }
7603
+ }
7604
+ },
7079
7605
  "V2BillingRun": {
7080
7606
  "description": "V2 billing run",
7081
7607
  "content": {
@@ -9,7 +9,7 @@ Askell POSTs signed JSON to each URL you register. Verify \`Hook-HMAC\` before p
9
9
  Headers:
10
10
  - Hook-HMAC: base64 HMAC-SHA512 of the **raw body** (secret = \`hmac_secret\` from webhook create)
11
11
  - Hook-Event: event type (\`subscription.renewed\`, \`payment.changed\`, or a family wildcard \`subscription.*\`)
12
- - Hook-API-Version: \`v1\` for plan/subscription/customer/payment/checkout, \`v2\` for subscription_contract / billing_run / fulfillment_order
12
+ - Hook-API-Version: \`v1\` for plan/subscription/customer/checkout and one-off \`payment.*\`; \`v2\` for subscription_contract / billing_run / fulfillment_order and for \`payment.*\` of a billing-run charge
13
13
 
14
14
  ## Body shape
15
15
 
@@ -17,7 +17,7 @@ JSON body **is the event object**. It is **not** \`{ event, data }\`.
17
17
 
18
18
  Upstream swagger used to document a dummy \`POST /your-webhook-url/\` with \`SubscriptionMultiLite\` (\`{ customer, subscriptions[] }\`). \`sync-specs\` strips that path.
19
19
 
20
- Most inbound families are still undocumented in OpenAPI — this resource is the overlay. Exception: \`fulfillment_order.*\` body **is** \`V2FulfillmentOrder\` (same as \`GET /v2/fulfillment-orders/{id}/\`). Live https://docs.askell.is/en/api/webhooks.html does not list this family yet.
20
+ Most inbound families are still undocumented in OpenAPI — this resource is the overlay. Exception: \`fulfillment_order.*\` body **is** \`V2FulfillmentOrder\` (same as \`GET /v2/fulfillment-orders/{id}/\`). Live https://docs.askell.is/en/api/webhooks.html lists that family (\`created\`, \`shipment_booked\`, \`fulfilled\`, \`cancelled\`).
21
21
 
22
22
  Rare historical payloads used \`{ event, data, ref?, sender? }\`. If both \`event\` and \`data\` are objects, use \`data\`.
23
23
 
@@ -42,28 +42,52 @@ Live REST \`Subscription\` objects have extra fields the OpenAPI schema omits. W
42
42
  V2 migration: \`subscription.*\` is **not** aliased onto the new contract (payload is \`SubscriptionContract\`). \`subscription.canceled\` is not sent merely because billing moved to a V2 contract.
43
43
 
44
44
  ### subscription_contract.* (v2)
45
- \`created\`, \`changed\`, \`renewed\`, \`migrated\`
45
+ \`created\`, \`changed\`, \`renewed\`, \`migrated\`. Live https://docs.askell.is/en/api/webhooks.html also names \`activated\`, \`paused\`, \`resumed\`, \`canceled\`, \`ended\`.
46
46
 
47
- \`id\`, \`customer\`, \`state\`, \`billing_anchor_at\`, \`next_billing_at\`, \`cancel_at\`, \`cancel_at_period_end\`, \`canceled_at\`, \`ended_at\`, \`currency\`, \`recurring\`, \`legacy_subscription\`, \`legacy_subscription_ids\`, \`migration_effective_at\`, \`billing_managed_by\`, \`created_at\`, \`updated_at\`.
47
+ A scheduled cancellation sends \`subscription_contract.ended\`, not \`canceled\`, even though \`state\` becomes \`canceled\`. \`canceled\` is only an immediate cancel. \`changed\` often arrives next to the more specific event.
48
+
49
+ Except \`migrated\`, the body is the contract as GET \`/v2/subscription-contracts/{id}/\` returns it (\`V2SubscriptionContract\`). \`customer\` is an object; the numeric id is \`customer_id\`. The page also puts \`reference\` and \`scheduled_changes\` on that body, and a pending item change on item \`scheduled_change\` (the example JSON omits \`scheduled_changes\`; the following paragraph names it). The example includes \`legacy_source_mappings\`, which OpenAPI does not list.
50
+
51
+ \`migrated\` is not the whole contract, and \`customer\` there is a numeric id. Fields: \`id\` / \`subscription_contract_id\` (same value), \`migration_batch_id\`, \`customer\`, \`customer_reference\`, \`billing_managed_by\`, \`migration_effective_at\`, \`legacy_subscription_ids\`, \`legacy_source_mappings\`.
52
+
53
+ \`subscription_contract_scheduled_change.*\` and \`subscription_contract_item.*\` are not included in this family.
54
+
55
+ ### subscription_contract_item.* (v2)
56
+ Not covered by \`subscription_contract.*\`. Register \`subscription_contract_item.*\` or \`subscription_contract_item.entitlement_changed\`.
57
+
58
+ Sent when an item's entitlement changes (\`service_active\` / \`service_state\`, \`entitled_until\`, quantity, or product). \`id\` and \`contract_item_id\` are the item id. Also \`subscription_contract_id\`, numeric \`customer\`, \`customer_reference\`, \`changed_fields\`, and \`previous\` (entitlement fields before the change).
59
+
60
+ ### subscription_contract_scheduled_change.* (v2)
61
+ Not covered by \`subscription_contract.*\`. Register \`subscription_contract_scheduled_change.*\` or a specific event.
62
+
63
+ \`subscription_contract_scheduled_change.created\` — item change scheduled for the next renewal (\`apply_at=period_end\`). \`subscription_contract_scheduled_change.applied\` — applied when that renewal's billing run is created. \`subscription_contract_scheduled_change.canceled\` — canceled via the API, or automatically when the item is removed or the contract is canceled.
64
+
65
+ Payload is the scheduled change (same shape as \`scheduled_changes[]\`) plus \`subscription_contract_id\`, \`customer\` (numeric id), \`customer_reference\`. \`operation\` is \`update_item\`. \`state\` is \`scheduled\` / \`applied\` / \`canceled\`. \`applied_at\`, \`applied_change_id\`, and \`billing_run_id\` are set only on \`applied\`. \`canceled_at\` is set only on \`canceled\`.
48
66
 
49
67
  ### billing_run.* (v2)
50
68
  \`created\`, \`changed\`, \`succeeded\`, \`failed\`, \`retry\`
51
69
 
52
- \`id\`, \`contract\`, \`period_start_at\`, \`period_end_at\`, \`state\`, \`currency\`, \`subtotal_amount\`, \`tax_amount\`, \`total_amount\`, \`attempt_count\`, \`max_attempts\`, \`next_retry_at\`, \`last_attempt_at\`, \`transaction\`, \`created_at\`, \`updated_at\`.
70
+ Body matches \`V2BillingRun\`: \`contract_id\` (not \`contract\`), \`customer_id\`, \`period_start_at\`, \`period_end_at\`, \`state\`, amounts, \`attempt_count\`, \`max_attempts\`, \`next_retry_at\`, \`last_attempt_at\`, \`transaction_id\`, \`transaction_uuid\`, \`transaction_external_reference\` (null when no transaction exists), \`lines\`, \`attempts\`. Live page \`state\` values: \`scheduled\`, \`processing\`, \`retry_scheduled\`, \`pending_external\`, \`succeeded\`, \`failed_terminal\`, \`canceled\`, \`voided\`, \`refunded\`. OpenAPI leaves \`state\` an unconstrained string.
71
+
72
+ Refunding a succeeded run (\`POST /v2/billing-runs/{id}/refund/\`) sends \`billing_run.changed\`, not a new event type. The run's \`state\` becomes \`refunded\`. A \`202\` means the run is still \`succeeded\`.
53
73
 
54
74
  ### customer.* (v1)
55
75
  \`created\`, \`changed\` — same shape as GET \`/customers/{ref}/\` (\`id\`, names, \`email\`, \`phone\`, \`customer_reference\`, address fields, \`payment_method[]\`).
56
76
 
57
- ### payment.* (v1)
58
- \`created\`, \`changed\`, \`retry\`
77
+ ### payment.*
78
+ \`created\`, \`changed\`, \`retry\`. One registration receives both shapes. Tell them apart with \`Hook-API-Version\`, or with \`subscription_contract_id\` (only on a billing-run charge).
79
+
80
+ One-off Payment (\`Hook-API-Version: v1\`): \`uuid\`, \`amount\`, \`currency\`, \`description\`, \`reference\`, \`state\` (\`pending\` | \`settled\` | \`failed\` | \`retrying\`), \`created_at\`, \`updated_at\`, \`transactions[]\`.
81
+
82
+ Billing-run charge (\`Hook-API-Version: v2\`): a flat transaction, no \`transactions[]\`. \`uuid\` (transaction id), \`amount\`, \`currency\`, \`description\`, \`reference\` (processor reference), \`state\` (\`initial\` | \`pending\` | \`settled\` | \`failed\` | \`canceled\` | \`refunded\`), \`fail_code\`, \`refund_code\`, \`cancel_code\`, \`payment_method\` (numeric id), \`billing_run_id\`, \`billing_run_attempt_id\`, \`subscription_contract_id\`, \`customer\`, \`customer_reference\`, \`legacy_subscription_ids\`. \`payment.created\` when an attempt opens a transaction, \`payment.changed\` when the charge succeeds, fails permanently, or waits on the processor, \`payment.retry\` when another attempt is scheduled. A \`payment.retry\` with no transaction has null \`uuid\`, \`reference\`, \`created_at\`, \`updated_at\`, \`payment_method\`, and codes; \`amount\` / \`currency\` come from the run, and \`state\` is the run's state (for example \`retry_scheduled\`).
59
83
 
60
- \`uuid\`, \`amount\`, \`currency\`, \`description\`, \`reference\`, \`state\` (\`pending\` | \`settled\` | \`failed\` | \`retrying\`), \`created_at\`, \`updated_at\`, \`transactions[]\`.
84
+ That \`uuid\` is not a Payment. Do not \`POST /payments/{uuid}/refund/\`. Refund with \`POST /v2/billing-runs/{billingRunId}/refund/\`.
61
85
 
62
86
  ### checkout.* (v1)
63
87
  \`created\`, \`changed\` — \`token\`, \`checkout_url\`, \`status\`.
64
88
 
65
89
  ### fulfillment_order.* (v2)
66
- Family wildcard \`fulfillment_order.*\`. Bundled swagger names \`fulfillment_order.fulfilled\` (\`POST .../fulfill/\` or dashboard ship) and \`fulfillment_order.cancelled\` (\`POST .../cancel/\`). Do not invent \`created\`/\`changed\`. Live https://docs.askell.is/en/api/webhooks.html still omits this family.
90
+ Live https://docs.askell.is/en/api/webhooks.html names \`fulfillment_order.created\` (after payment succeeds), \`fulfillment_order.shipment_booked\` (tracking is available; the payload also has \`shipment_id\` pointing at that entry in \`fulfillments[]\`), \`fulfillment_order.fulfilled\`, and \`fulfillment_order.cancelled\`. Swagger operation text only names \`fulfilled\` and \`cancelled\`. Do not invent \`changed\`.
67
91
 
68
92
  Body = \`V2FulfillmentOrder\` = \`GET /v2/fulfillment-orders/{fulfillmentOrderId}/\` (list items are the same object). Physical order from a paid billing run (\`billing_run_id\`; at most one order per run). \`delivery_address\` is a snapshot (later contract address edits do not change it). \`shipping_selection\` is the checkout snapshot. \`fulfillments[]\` are booked shipments (empty until booked / if no shipping providers). Status: \`open\` | \`partially_fulfilled\` | \`fulfilled\` | \`cancelled\`. External carrier with no Askell integration: shipment \`handler\` is \`""\` — read \`carrier\`.
69
93
 
package/src/server.ts CHANGED
@@ -36,7 +36,7 @@ Workflow:
36
36
  3. Use askell_call (GET/HEAD) or askell_mutate (POST/PUT/PATCH/DELETE) when no dedicated tool covers the request.
37
37
 
38
38
  API models:
39
- - v1 (legacy): PlanVariant + Subscription at paths like /subscriptions/, /customers/. Still supported for existing integrations.
39
+ - v1 (legacy): PlanVariant + Subscription at paths like /subscriptions/, /customers/. Customer, webhook, and one-off payment writes are not this guard. Legacy subscription writes can 400 with LegacySubscriptionGuardError — match \`code\`, not the message (askell_describe_operation omits response descriptions). \`legacy_subscriptions_disabled\`: contracts-only account, on POST /subscriptions/multi/ (customer is neither created nor updated), POST /customers/{ref}/subscriptions/add/ (that body also has \`status: error\`), and POST /checkouts/ with a plan (payment_processor checkout still works). \`subscription_managed_by_contract\`: that subscription's billing moved; PATCH/cancel/activate/set_expiry. Body has migrated_to_contract_id and v2_endpoint.
40
40
  - v2 (current): Catalog, bundles, quotes, checkouts, subscription contracts, billing runs, coupons/promotion codes, fulfillment orders under /v2/. Prefer v2 for new integrations.
41
41
  - Prose docs at https://docs.askell.is/api/ may describe flows (embedded checkout, 3D Secure, wallet passes) not fully listed in OpenAPI.
42
42
 
@@ -59,6 +59,18 @@ V2 checkout notes:
59
59
  - Hosted POST /v2/checkouts/: shipping {option, location_id?} is required when the offer has physical products and the account has active shipping options. No shipping-options list in OpenAPI (ids are account config). Pickup options need location_id. Snapshot is contract.shipping_selection (plus location / zone_name / weight_band), not on V2Checkout. Rate-table option with no zip/weight rate: 400, shipping_code shipping_not_available. Quote/checkout totals already include shipping_fee when present.
60
60
  - Hosted iframe (not askell.js): POST /v2/checkouts/ and POST .../payment-method-registrations/ take allowed_origin (one origin, no path; http only localhost/loopback). Replaces account-level frame-ancestors; GET empty string = account-level. Rejected on /v2/checkout-sessions/ (sales-channel allowed_origins[]).
61
61
  - Embedded checkout uses POST /v2/checkout-sessions/ plus browser session-token sub-paths (widget collects address/shipping; see docs, not all in OpenAPI).
62
+ - Checkout/session metadata is copied onto the contract at finalize except Askell-owned keys (askell_source, billing_anchor_mode, activation_failure, email_markers, copied_legacy_pauses, copied_legacy_extra_data, migration_source, migration_cadence_mode, legacy_subscription_ids, seed). On a shared key the session value wins.
63
+
64
+ V2 contract changes:
65
+ - reference: external id, max 128, no commas. Create: blank means none. PATCH null/blank clears. GET /v2/subscription-contracts/?reference= is an exact filter.
66
+ - PATCH body is only metadata, reference, payment_processor_override. The operation description also lists delivery_address, accounting_department, accounting_cost_center; V2SubscriptionContractPatch does not include them — do not send them.
67
+ - PATCH metadata replaces the integration's keys. The Askell-owned keys above keep their current values; sending them does nothing.
68
+ - items/update apply_at=now (default): effective_at must not be in the future (400 future_effective_at_not_supported). apply_at=period_end charges nothing now and stores scheduled_change (change_id null); do not send effective_at, settlement_behavior, or include_pending_adjustments with it. A pending scheduled change rejects further item updates with 409 scheduled_change_exists until POST .../scheduled-changes/{scheduledChangeId}/cancel/ (already canceled → replayed: true; applied/failed → 409 scheduled_change_not_cancelable). items/add and items/remove can also 409 (V2ProrationConflict); their operation text does not say so. Webhook families subscription_contract_scheduled_change.* and subscription_contract_item.* are not part of subscription_contract.*. Non-migrated contract events match GET: customer is an object, numeric id is customer_id. A scheduled cancellation sends subscription_contract.ended, not canceled, even though state becomes canceled.
69
+ - An immediate billing-interval change with an amount due sets item.pending_interval_change status awaiting_payment (applies_on_payment). The item switches only when that billing run succeeds. Other item edits and change-anchor return 409 pending_interval_change until then. A failed payment abandons the interval change; retrying the run does not apply it.
70
+ - POST .../change-anchor/ moves the next renewal of the contract and every active item. new_billing_anchor_at must be after effective_at and, with proration, at most one billing period later. Does not extend entitlements. Preview with proration-preview operation=change_anchor (new_billing_anchor_at required). Do not PATCH billing_anchor_at.
71
+
72
+ V2 refunds:
73
+ - A billing-run charge is not a Payment. payment.* for that charge is a flat object (Hook-API-Version v2, subscription_contract_id, billing_run_id, billing_run_attempt_id), not transactions[]; a payment.retry may have a null uuid and state retry_scheduled. Refund with POST /v2/billing-runs/{id}/refund/ (no body, full amount only, secret). 200 → state refunded, metadata.transaction_refund, webhook billing_run.changed (no separate refund event). 202 → run still succeeded (metadata.refund_requests); wait or re-GET; do not resend immediately. 400 if not succeeded, zero amount, already refunded, or the processor cannot refund. No response (timeout): GET the run and check state plus metadata.refund_requests before retrying. POST /payments/{uuid}/refund/ is one-off Payments only.
62
74
 
63
75
  V2 fulfillment (warehouse):
64
76
  - GET /v2/fulfillment-orders/ and GET /v2/fulfillment-orders/{id}/. Same body as fulfillment_order.* webhooks (V2FulfillmentOrder). Secret key. 403 if contracts/shipping off. Poll updated_since after a missed webhook (newest first).
@@ -218,7 +218,7 @@ export function registerAnalysisTools(
218
218
  {
219
219
  title: 'Subscription contract overview (v2)',
220
220
  description:
221
- 'Fetch a v2 subscription contract and recent billing runs filtered by contract id. The contract payload includes `discount` (active coupon) when one is applied, `shipping_selection` when shipping was chosen at checkout, and `subscriber_page` (customer-facing management URL, read-only). Result is an error when the contract fetch fails. `failures` lists every call that failed.',
221
+ 'Fetch a v2 subscription contract and recent billing runs filtered by contract id. The contract payload includes `discount` (active coupon) when one is applied, `shipping_selection` when shipping was chosen at checkout, `subscriber_page` (customer-facing management URL, read-only), `reference`, `scheduled_changes`, and per-item `scheduled_change` / `pending_interval_change` (status `awaiting_payment` while an interval change waits for its charge). Result is an error when the contract fetch fails. `failures` lists every call that failed.',
222
222
  inputSchema: z.object({
223
223
  contractId: z
224
224
  .union([z.string().min(1), z.int()])