Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Changelog

## Unreleased
## 0.1.0a3 — 2026-11-13

- Sync `openapi.yaml` with the production specification.
- Add `OrderLine.related_resources.subscription_id`: the subscription associated with a
signup, renewal or immediate-update line. `related_resources` is `None` on historical
and unrelated lines and when omitted; `subscription_id` is `None` on a signup line
whose subscription is not yet available. `product_type`/`product_id` are unchanged.
- Clarify European positioning, installation and release documentation.

## 0.1.0a2 — 2026-11-12
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ OpenAPI operations are covered.
## Installation

```bash
pip install vatly-api-python==0.1.0a2
pip install vatly-api-python==0.1.0a3
```

For local development: `uv sync --locked`.
Expand Down
7 changes: 4 additions & 3 deletions docs/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,14 @@ logging response bodies that may contain personal data.

| Source | Revision |
| --- | --- |
| Production OpenAPI, retrieved 2026-11-09 | [vatly-docs specification](https://github.com/vatly/vatly-docs/blob/7e71ade32d311b10f79e10c15d0ac03ae32bde97/public/openapi.yaml) |
| Production OpenAPI, retrieved 2026-11-13 | [vatly-docs specification](https://github.com/vatly/vatly-docs/blob/66c8205ea886e4cb48e8e3b2e988de3839c6d4d8/public/openapi.yaml) |
| Original JS design reference | [SDK revision](https://github.com/vatly/vatly-api-js/commit/8b0b920c29f7e7b30bf71b9e379ec0a8a1a22028) |

The checked-in specification is byte-identical to that source. Its SHA-256 is:
The checked-in specification is byte-identical to that source and to the bundle
published by Vatly's production release. Its SHA-256 is:

```text
69640ac82bbba8d101e6aa882facd5f13aa5cf67f38508e5cea18abef03c4940
5e648169e101edc8b3e088dc0e28d0f0a293e8a0ed057014c7336057d986def9
```

The specification's generated-at date is not its retrieval date.
Expand Down
70 changes: 62 additions & 8 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,10 @@ paths:
description: |
Retrieves a specific order by its ID.
Includes full order details with line items, totals, and billing information.
For a paid checkout, follow `Checkout.orderId` and inspect `lines[].relatedResources`.
Signup, renewal and immediate-update lines expose the same subscriptionId.
Product references are unchanged. Neither
creation, a paid order nor an ID alone proves activation or entitlement.
tags:
- Orders
parameters:
Expand Down Expand Up @@ -2009,7 +2013,7 @@ paths:
lines:
- id: order_item_Jk4pQrSvWm8NjLhYbUcP
resource: orderline
description: Pro Monthly Subscription
description: Pro Monthly Subscription — initial checkout
quantity: 1
basePrice:
value: '29.00'
Expand All @@ -2028,8 +2032,10 @@ paths:
amount:
value: '6.09'
currency: EUR
productType: subscription
productId: subscription_Hn5xWqVfKm8RjTgYbUcP
productType: subscription_plan
productId: subscription_plan_Hn5xWqVfKm8RjTgYbUcP
relatedResources:
subscriptionId: subscription_Hn5xWqVfKm8RjTgYbUcP
customerDetails:
fullName: John Doe
companyName: Acme Corp
Expand Down Expand Up @@ -6767,7 +6773,14 @@ components:
and expires after 72 hours; an expired link lets the customer request a fresh one by email.
OrderLine:
type: object
description: A line item in an order
description: |
A line item in an order. `productType`/`productId` retain their existing meaning:
initial subscription lines reference the plan; renewal and immediate-update
lines reference the existing subscription. `relatedResources` adds the exact
per-line subscription relationship without repeating the product reference.
Discounts remain components of their owning line.
Follow a paid Checkout's `orderId` to its order lines; Checkout is unchanged.
No historical backfill is performed.
required:
- id
- resource
Expand Down Expand Up @@ -6822,14 +6835,55 @@ components:
- billing_details_verification
- auth_request_product
- null
description: The product type this line links to; null when the line has no product link.
example: subscription
description: |
Linked resource type. Initial subscription orders use
`subscription_plan`; renewals and immediate updates use `subscription`.
Existing values and new-write behavior are unchanged.
example: subscription_plan
productId:
type:
- string
- 'null'
description: The linked product's id; for a `subscription` line this is the public `subscription_…` id.
example: subscription_Hn5xWqVfKm8RjTgYbUcP
description: |
Linked resource ID: a plan ID when productType is subscription_plan,
a subscription ID when productType is subscription. Existing values and
new-write behavior are unchanged.
example: subscription_plan_Hn5xWqVfKm8RjTgYbUcP
relatedResources:
description: |
Additional per-line relationships. Null on historical lines without recorded
correlation and on unrelated lines (including one-off, billing-verification
and authorization-minimum lines). Older payloads may omit this property.
Signup, renewal and immediate-update lines use the same subscriptionId:
the subscription associated with this line, not a creation or entitlement
signal. On ordinary sale lines productType distinguishes initial signup
(subscription_plan) from renewal/update (subscription). It does not
distinguish a renewal from an immediate update. Reversal lines copy the
original relationship and product reference, not a new creation claim.
oneOf:
- type: object
additionalProperties: false
required:
- subscriptionId
properties:
subscriptionId:
type:
- string
- 'null'
description: |
Subscription associated with this line. On initial signup lines,
null while the exact subscription projection is unavailable,
including pending or skipped creation; null does not distinguish
these cases and is not proof that creation failed or was skipped.
The deterministic per-line identity is retained durably, so GET
reveals the ID after projection recovery without recreating the
paid order. Frozen webhook deliveries retain their original snapshot;
GET and newly generated webhooks use the current availability.
Separate same-plan checkout lines have distinct subscription IDs;
quantity is the quantity of one subscription, not a subscription count.
Creation does not imply activation, entitlement or immediate readability.
Validate ownership and status before granting access.
- type: 'null'
InvoiceUpdateLink:
type: object
description: A signed URL for the customer to update invoice details (billing address, VAT number, company name)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "vatly-api-python"
version = "0.1.0a2"
version = "0.1.0a3"
description = "Python SDK for Vatly, the European Merchant of Record for AI, software and digital products"
keywords = ["vatly", "merchant-of-record", "billing", "payments", "vat", "ai", "sdk"]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/vatly/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.0a2"
__version__ = "0.1.0a3"
51 changes: 46 additions & 5 deletions src/vatly/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,33 @@ class Links4(VatlyModel):
]


class RelatedResources(VatlyModel):
"""
Additional per-line relationships. Null on historical lines without recorded
correlation and on unrelated lines (including one-off, billing-verification
and authorization-minimum lines). Older payloads may omit this property.
Signup, renewal and immediate-update lines use the same subscriptionId:
the subscription associated with this line, not a creation or entitlement
signal. On ordinary sale lines productType distinguishes initial signup
(subscription_plan) from renewal/update (subscription). It does not
distinguish a renewal from an immediate update. Reversal lines copy the
original relationship and product reference, not a new creation claim.

"""

model_config = ConfigDict(
extra="allow",
populate_by_name=True,
)
subscription_id: Annotated[
str | None,
Field(
alias="subscriptionId",
description="Subscription associated with this line. On initial signup lines,\nnull while the exact subscription projection is unavailable,\nincluding pending or skipped creation; null does not distinguish\nthese cases and is not proof that creation failed or was skipped.\nThe deterministic per-line identity is retained durably, so GET\nreveals the ID after projection recovery without recreating the\npaid order. Frozen webhook deliveries retain their original snapshot;\nGET and newly generated webhooks use the current availability.\nSeparate same-plan checkout lines have distinct subscription IDs;\nquantity is the quantity of one subscription, not a subscription count.\nCreation does not imply activation, entitlement or immediate readability.\nValidate ownership and status before granting access.\n",
),
] = None


class InvoiceUpdateLink(VatlyModel):
"""
A signed URL for the customer to update invoice details (billing address, VAT number, company name)
Expand Down Expand Up @@ -2117,7 +2144,14 @@ class CreateSubscriptionBillingUpdateLinkResponse(VatlyModel):

class OrderLine(VatlyModel):
"""
A line item in an order
A line item in an order. `productType`/`productId` retain their existing meaning:
initial subscription lines reference the plan; renewal and immediate-update
lines reference the existing subscription. `relatedResources` adds the exact
per-line subscription relationship without repeating the product reference.
Discounts remain components of their owning line.
Follow a paid Checkout's `orderId` to its order lines; Checkout is unchanged.
No historical backfill is performed.

"""

model_config = ConfigDict(
Expand Down Expand Up @@ -2163,16 +2197,23 @@ class OrderLine(VatlyModel):
| None,
Field(
alias="productType",
description="The product type this line links to; null when the line has no product link.",
examples=["subscription"],
description="Linked resource type. Initial subscription orders use\n`subscription_plan`; renewals and immediate updates use `subscription`.\nExisting values and new-write behavior are unchanged.\n",
examples=["subscription_plan"],
),
] = None
product_id: Annotated[
str | None,
Field(
alias="productId",
description="The linked product's id; for a `subscription` line this is the public `subscription_…` id.",
examples=["subscription_Hn5xWqVfKm8RjTgYbUcP"],
description="Linked resource ID: a plan ID when productType is subscription_plan,\na subscription ID when productType is subscription. Existing values and\nnew-write behavior are unchanged.\n",
examples=["subscription_plan_Hn5xWqVfKm8RjTgYbUcP"],
),
] = None
related_resources: Annotated[
RelatedResources | None,
Field(
alias="relatedResources",
description="Additional per-line relationships. Null on historical lines without recorded\ncorrelation and on unrelated lines (including one-off, billing-verification\nand authorization-minimum lines). Older payloads may omit this property.\nSignup, renewal and immediate-update lines use the same subscriptionId:\nthe subscription associated with this line, not a creation or entitlement\nsignal. On ordinary sale lines productType distinguishes initial signup\n(subscription_plan) from renewal/update (subscription). It does not\ndistinguish a renewal from an immediate update. Reversal lines copy the\noriginal relationship and product reference, not a new creation claim.\n",
),
] = None

Expand Down
30 changes: 30 additions & 0 deletions tests/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,36 @@ def handler(r):
)


def test_order_line_related_subscription_is_typed_and_nullable(spec):
order = example(spec["components"]["schemas"]["Order"], spec)
line = order["lines"][0]
order["lines"] = [
{
**line,
"productType": "subscription_plan",
"productId": "subscription_plan_a",
"relatedResources": {"subscriptionId": "subscription_a"},
},
{**line, "productType": "subscription_plan", "relatedResources": {"subscriptionId": None}},
{**line, "productType": "one_off_product", "relatedResources": None},
line, # Older payloads omit relatedResources.
]
with VatlyClient(
KEY, transport=httpx.MockTransport(lambda r: httpx.Response(200, json=order))
) as c:
signup, unresolved, unrelated, older = c.orders.get("order_a").lines
assert "relatedResources" not in line
assert signup.related_resources.subscription_id == "subscription_a"
assert signup.product_id == "subscription_plan_a"
assert unresolved.related_resources is not None
assert unresolved.related_resources.subscription_id is None
assert unrelated.related_resources is None
assert older.related_resources is None
wire = [ln.model_dump(by_alias=True, exclude_unset=True) for ln in (signup, unresolved)]
assert wire[0]["relatedResources"] == {"subscriptionId": "subscription_a"}
assert wire[1]["relatedResources"] == {"subscriptionId": None}


@pytest.mark.parametrize(
"status,error",
[
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading