vatly-api-python 0.1.0a2__py3-none-any.whl

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.
vatly/models.py ADDED
@@ -0,0 +1,3009 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: schema.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Mapping, Sequence
7
+ from datetime import date
8
+ from typing import Annotated, Any, Literal
9
+
10
+ from pydantic import AwareDatetime, ConfigDict, Field, RootModel
11
+
12
+ from vatly._model import VatlyModel
13
+
14
+
15
+ class Model(RootModel[Any]):
16
+ model_config = ConfigDict(
17
+ populate_by_name=True,
18
+ )
19
+ root: Any
20
+
21
+
22
+ class Money1(VatlyModel):
23
+ """
24
+ Monetary value with currency
25
+ """
26
+
27
+ model_config = ConfigDict(
28
+ extra="allow",
29
+ populate_by_name=True,
30
+ )
31
+ value: Annotated[
32
+ str,
33
+ Field(
34
+ description='The amount as a decimal string. Sent and returned as a string to\npreserve precision for financial calculations.\n\nThe number of required decimal places is **currency-dependent**: it\nequals the currency\'s ISO 4217 minor-unit count — the same rule the\nserver enforces via [moneyphp/money](https://github.com/moneyphp/money)\'s\n`DecimalMoneyParser`. The `(value, currency)` pair is validated\ntogether (see the `oneOf` below); mixing precisions is rejected:\n\n- 2 decimals — EUR, USD (and most currencies): `"10.00"` ✓, `"10"` ✗, `"10.0"` ✗, `"10.000"` ✗\n- 0 decimals — e.g. JPY: `"1000"` ✓, `"1000.00"` ✗ *(not yet enabled)*\n- 3 decimals — e.g. BHD, KWD, OMR, TND: `"1.000"` ✓, `"1.00"` ✗ *(not yet enabled)*\n\nBecause the value is a decimal amount (not minor units), `"1000"`\nmeans one thousand units — valid only for a 0-decimal currency; for\nEUR/USD you must write `"1000.00"`.\n',
35
+ examples=["99.99"],
36
+ pattern="^\\d+\\.\\d{2}$",
37
+ ),
38
+ ]
39
+ currency: Annotated[
40
+ Literal["EUR", "USD"],
41
+ Field(
42
+ description="ISO 4217 currency code. Vatly aligns its supported currency set\nwith its payment provider's; currencies are enabled as PSP support and\ndownstream tax/FX coverage are in place. More currencies may be added\nlater.\n",
43
+ examples=["EUR"],
44
+ ),
45
+ ]
46
+
47
+
48
+ class Money(RootModel[Money1]):
49
+ model_config = ConfigDict(
50
+ populate_by_name=True,
51
+ )
52
+ root: Annotated[Money1, Field(description="Monetary value with currency")]
53
+
54
+
55
+ class EurMoney(VatlyModel):
56
+ """
57
+ Monetary value in EUR. Used where the API currently accepts EUR prices
58
+ only: catalog prices and checkout price overrides. The `value` takes
59
+ exactly two decimals.
60
+
61
+ """
62
+
63
+ model_config = ConfigDict(
64
+ extra="allow",
65
+ populate_by_name=True,
66
+ )
67
+ value: Annotated[
68
+ str,
69
+ Field(
70
+ description="The amount as a decimal string with exactly two decimals.",
71
+ examples=["99.99"],
72
+ pattern="^\\d+\\.\\d{2}$",
73
+ ),
74
+ ]
75
+ currency: Annotated[
76
+ Literal["EUR"],
77
+ Field(
78
+ description="ISO 4217 currency code. Only `EUR` is accepted.",
79
+ examples=["EUR"],
80
+ ),
81
+ ]
82
+
83
+
84
+ class Link(VatlyModel):
85
+ """
86
+ HATEOAS link to a related resource
87
+ """
88
+
89
+ model_config = ConfigDict(
90
+ extra="allow",
91
+ populate_by_name=True,
92
+ )
93
+ href: Annotated[
94
+ str,
95
+ Field(
96
+ description="URL of the linked resource",
97
+ examples=["https://api.vatly.com/v1/orders/order_Hn5xWqVfKm8RjTgYbUcP"],
98
+ ),
99
+ ]
100
+ type: Annotated[
101
+ Literal["application/json", "text/html"],
102
+ Field(
103
+ description="Content type of the linked resource.\n- `application/json` for API resources\n- `text/html` for hosted pages\n",
104
+ examples=["application/json"],
105
+ ),
106
+ ]
107
+
108
+
109
+ class BillingAddress(VatlyModel):
110
+ """
111
+ Customer or merchant billing address
112
+ """
113
+
114
+ model_config = ConfigDict(
115
+ extra="allow",
116
+ populate_by_name=True,
117
+ )
118
+ full_name: Annotated[
119
+ str | None,
120
+ Field(
121
+ alias="fullName",
122
+ description="Full name of the person or contact",
123
+ examples=["John Doe"],
124
+ ),
125
+ ] = None
126
+ company_name: Annotated[
127
+ str | None,
128
+ Field(
129
+ alias="companyName",
130
+ description="Company or organization name",
131
+ examples=["Acme Corp"],
132
+ ),
133
+ ] = None
134
+ tax_id: Annotated[
135
+ str | None,
136
+ Field(
137
+ alias="taxId",
138
+ description="Tax identification number for tax purposes",
139
+ examples=["NL123456789B01"],
140
+ ),
141
+ ] = None
142
+ street_and_number: Annotated[
143
+ str | None,
144
+ Field(
145
+ alias="streetAndNumber",
146
+ description="Street address including building number",
147
+ examples=["123 Main Street"],
148
+ ),
149
+ ] = None
150
+ street_additional: Annotated[
151
+ str | None,
152
+ Field(
153
+ alias="streetAdditional",
154
+ description="Additional address information (apartment, suite, etc.)",
155
+ examples=["Suite 456"],
156
+ ),
157
+ ] = None
158
+ city: Annotated[str | None, Field(description="City name", examples=["Amsterdam"])] = None
159
+ region: Annotated[
160
+ str | None,
161
+ Field(description="State, province, or region", examples=["North Holland"]),
162
+ ] = None
163
+ postal_code: Annotated[
164
+ str | None,
165
+ Field(alias="postalCode", description="Postal or ZIP code", examples=["1012 AB"]),
166
+ ] = None
167
+ country: Annotated[
168
+ str | None,
169
+ Field(
170
+ description="ISO 3166-1 alpha-2 country code",
171
+ examples=["NL"],
172
+ max_length=2,
173
+ min_length=2,
174
+ ),
175
+ ] = None
176
+
177
+
178
+ class BillingDetails(VatlyModel):
179
+ """
180
+ Complete billing details including address and contact information
181
+ """
182
+
183
+ model_config = ConfigDict(
184
+ extra="allow",
185
+ populate_by_name=True,
186
+ )
187
+ full_name: Annotated[
188
+ str | None,
189
+ Field(
190
+ alias="fullName",
191
+ description="Full name of the person or contact",
192
+ examples=["John Doe"],
193
+ ),
194
+ ] = None
195
+ company_name: Annotated[
196
+ str | None,
197
+ Field(
198
+ alias="companyName",
199
+ description="Company or organization name",
200
+ examples=["Acme Corp"],
201
+ ),
202
+ ] = None
203
+ tax_id: Annotated[
204
+ str | None,
205
+ Field(
206
+ alias="taxId",
207
+ description="Tax identification number",
208
+ examples=["NL123456789B01"],
209
+ ),
210
+ ] = None
211
+ street_and_number: Annotated[
212
+ str | None,
213
+ Field(
214
+ alias="streetAndNumber",
215
+ description="Street address including building number",
216
+ examples=["123 Main Street"],
217
+ ),
218
+ ] = None
219
+ street_additional: Annotated[
220
+ str | None,
221
+ Field(
222
+ alias="streetAdditional",
223
+ description="Additional address information",
224
+ examples=["Suite 456"],
225
+ ),
226
+ ] = None
227
+ city: Annotated[str | None, Field(description="City name", examples=["Amsterdam"])] = None
228
+ region: Annotated[
229
+ str | None,
230
+ Field(description="State, province, or region", examples=["North Holland"]),
231
+ ] = None
232
+ postal_code: Annotated[
233
+ str | None,
234
+ Field(alias="postalCode", description="Postal or ZIP code", examples=["1012 AB"]),
235
+ ] = None
236
+ country: Annotated[
237
+ str | None,
238
+ Field(description="ISO 3166-1 alpha-2 country code", examples=["NL"]),
239
+ ] = None
240
+ email: Annotated[
241
+ str | None,
242
+ Field(
243
+ description="Email address.\nInternationalized domains must use their Punycode (ASCII) form\nper RFC 3492 — e.g. `billing@xn--mller-kva.de`.\n",
244
+ examples=["billing@acme.com"],
245
+ ),
246
+ ] = None
247
+
248
+
249
+ class Metadata(RootModel[Mapping[str, Any] | None]):
250
+ model_config = ConfigDict(
251
+ populate_by_name=True,
252
+ )
253
+ root: Annotated[
254
+ Mapping[str, Any] | None,
255
+ Field(
256
+ description="Arbitrary key-value metadata for your application.\nUp to 50 keys, with key names up to 40 characters and values up to 500 characters.\n",
257
+ examples=[{"orderId": "internal-12345", "campaign": "summer-sale"}],
258
+ ),
259
+ ]
260
+
261
+
262
+ class PaginationLinks(VatlyModel):
263
+ """
264
+ Pagination links for navigating list results
265
+ """
266
+
267
+ model_config = ConfigDict(
268
+ extra="allow",
269
+ populate_by_name=True,
270
+ )
271
+ self: Annotated[Link, Field(description="Link to the current page")]
272
+ next: Annotated[
273
+ Link | None,
274
+ Field(description="Link to the next page (null if on the last page)"),
275
+ ] = None
276
+ prev: Annotated[
277
+ Link | None,
278
+ Field(description="Link to the previous page (null if on the first page)"),
279
+ ] = None
280
+
281
+
282
+ class Error(VatlyModel):
283
+ """
284
+ Standard error response.
285
+
286
+ Every non-validation error path returns this shape — `message` is
287
+ always present, and `details` (if present) is an object carrying
288
+ structured context attached by the originating exception. HTTP
289
+ status code is the primary programmatic discriminator.
290
+
291
+ """
292
+
293
+ model_config = ConfigDict(
294
+ extra="allow",
295
+ populate_by_name=True,
296
+ )
297
+ message: Annotated[
298
+ str,
299
+ Field(
300
+ description="Human-readable error message",
301
+ examples=["The requested resource was not found."],
302
+ ),
303
+ ]
304
+ details: Annotated[
305
+ Mapping[str, Any] | None,
306
+ Field(
307
+ description="Optional structured context attached by the originating exception.\nShape depends on the specific error case; clients should treat\nunknown keys as forward-compatible additions and rely on the HTTP\nstatus code for branching logic.\n",
308
+ examples=[{"resource_type": "checkout", "resource_id": "checkout_abc123"}],
309
+ ),
310
+ ] = None
311
+
312
+
313
+ class ValidationError(VatlyModel):
314
+ """
315
+ Validation error response with field-specific errors
316
+ """
317
+
318
+ model_config = ConfigDict(
319
+ extra="allow",
320
+ populate_by_name=True,
321
+ )
322
+ message: Annotated[
323
+ str,
324
+ Field(
325
+ description="General validation error message",
326
+ examples=["The given data was invalid."],
327
+ ),
328
+ ]
329
+ errors: Annotated[
330
+ Mapping[str, Sequence[str]],
331
+ Field(
332
+ description="Field-specific validation errors.\nKeys are field names (using dot notation for nested fields).\nValues are arrays of error messages.\n",
333
+ examples=[
334
+ {
335
+ "email": ["The email field is required."],
336
+ "products.0.id": ["The selected products.0.id is invalid."],
337
+ }
338
+ ],
339
+ ),
340
+ ]
341
+
342
+
343
+ class Links(VatlyModel):
344
+ """
345
+ HATEOAS links to related resources
346
+ """
347
+
348
+ model_config = ConfigDict(
349
+ extra="allow",
350
+ populate_by_name=True,
351
+ )
352
+ checkout_url: Annotated[
353
+ Link,
354
+ Field(alias="checkoutUrl", description="Hosted checkout page URL (text/html)"),
355
+ ]
356
+ self: Annotated[Link, Field(description="Link to this checkout resource")]
357
+ order: Annotated[
358
+ Link | None,
359
+ Field(
360
+ description="Link to the created order. Always present; `null` until the checkout completes."
361
+ ),
362
+ ] = None
363
+
364
+
365
+ class Checkout(VatlyModel):
366
+ """
367
+ A checkout session represents a hosted payment flow.
368
+ Customers are redirected to the checkout URL to complete their purchase.
369
+
370
+ """
371
+
372
+ model_config = ConfigDict(
373
+ extra="allow",
374
+ populate_by_name=True,
375
+ )
376
+ id: Annotated[
377
+ str,
378
+ Field(
379
+ description="Unique identifier for the checkout",
380
+ examples=["checkout_QdEpFhdSrG4Y3DnfsdqsH"],
381
+ ),
382
+ ]
383
+ resource: Annotated[
384
+ Literal["checkout"],
385
+ Field(description="Resource type identifier", examples=["checkout"]),
386
+ ]
387
+ customer_id: Annotated[
388
+ str | None,
389
+ Field(
390
+ alias="customerId",
391
+ description="ID of the customer associated with this checkout.\nNull until a customer has been associated (for an anonymous\ncheckout this happens when the buyer completes payment).\n",
392
+ examples=["customer_7kBmRtPvXw2NjLhYcZaE"],
393
+ ),
394
+ ] = None
395
+ order_id: Annotated[
396
+ str | None,
397
+ Field(
398
+ alias="orderId",
399
+ description="ID of the order created from this checkout. Always present in the\npayload; `null` until the checkout completes, then set to the created\norder id.\n",
400
+ examples=["order_Tk7mNvBxKw2RjTgYcZaE"],
401
+ ),
402
+ ] = None
403
+ testmode: Annotated[
404
+ bool,
405
+ Field(description="Whether this checkout is in test mode", examples=[False]),
406
+ ]
407
+ redirect_url_success: Annotated[
408
+ str,
409
+ Field(
410
+ alias="redirectUrlSuccess",
411
+ description="URL to redirect customers after successful payment",
412
+ examples=["https://example.com/success"],
413
+ ),
414
+ ]
415
+ redirect_url_canceled: Annotated[
416
+ str,
417
+ Field(
418
+ alias="redirectUrlCanceled",
419
+ description="URL to redirect customers if they cancel the checkout",
420
+ examples=["https://example.com/canceled"],
421
+ ),
422
+ ]
423
+ metadata: Metadata | None = None
424
+ locale: Annotated[
425
+ Literal["en", "de", "fr", "nl", "es", "it", "pt", "pl"] | None,
426
+ Field(
427
+ description="The language you asked the hosted checkout to be presented in, as sent on\ncreation. `null` means you did not specify one, and the checkout picks a\nlanguage from the shopper's browser.\n",
428
+ examples=["de"],
429
+ ),
430
+ ]
431
+ status: Annotated[
432
+ Literal["created", "paid", "canceled", "failed", "expired"],
433
+ Field(
434
+ description="Current status of the checkout:\n- `created` - Checkout is active and awaiting payment\n- `paid` - Payment successful, order created\n- `canceled` - Checkout was canceled by the customer\n- `failed` - Payment failed\n- `expired` - Checkout expired without completion\n",
435
+ examples=["created"],
436
+ ),
437
+ ]
438
+ expires_at: Annotated[
439
+ AwareDatetime | None,
440
+ Field(
441
+ alias="expiresAt",
442
+ description="When this checkout will expire (ISO 8601 format)",
443
+ examples=["2024-01-15T14:30:00Z"],
444
+ ),
445
+ ] = None
446
+ created_at: Annotated[
447
+ AwareDatetime,
448
+ Field(
449
+ alias="createdAt",
450
+ description="When this checkout was created (ISO 8601 format)",
451
+ examples=["2024-01-15T10:30:00Z"],
452
+ ),
453
+ ]
454
+ links: Annotated[Links, Field(description="HATEOAS links to related resources")]
455
+
456
+
457
+ class CheckoutProduct(VatlyModel):
458
+ """
459
+ A product to add to the checkout
460
+ """
461
+
462
+ model_config = ConfigDict(
463
+ extra="allow",
464
+ populate_by_name=True,
465
+ )
466
+ id: Annotated[
467
+ str,
468
+ Field(
469
+ description="Product identifier. Must be either:\n- A one-off product ID (starts with `one_off_product_`)\n- A subscription plan ID (starts with `subscription_plan_`)\n",
470
+ examples=["one_off_product_Vr8kQdFhSrG4Y3DnfsdqH"],
471
+ pattern="^(one_off_product_|subscription_plan_)",
472
+ ),
473
+ ]
474
+ quantity: Annotated[
475
+ int | None, Field(description="Number of units to purchase", examples=[2], ge=1)
476
+ ] = 1
477
+ price: Annotated[
478
+ EurMoney | None,
479
+ Field(
480
+ description="Custom price override for this product, in EUR. If not provided, the\nproduct's default price is used.\n\nCurrently only EUR is accepted; any other currency returns a `422`.\nWhen the customer pays in another currency, the amount is converted\nfrom this EUR price at checkout. For a subscription, the converted\namount and currency are then fixed, so renewals charge the same amount.\n"
481
+ ),
482
+ ] = None
483
+ trial_days: Annotated[
484
+ int | None,
485
+ Field(
486
+ alias="trialDays",
487
+ description="Trial period in days for subscription plans.\nOnly applicable when `id` references a subscription plan.\n",
488
+ examples=[14],
489
+ ge=0,
490
+ ),
491
+ ] = None
492
+ metadata: Metadata | None = None
493
+
494
+
495
+ class CreateCheckoutRequest(VatlyModel):
496
+ """
497
+ Request body for creating a new checkout
498
+ """
499
+
500
+ model_config = ConfigDict(
501
+ extra="allow",
502
+ populate_by_name=True,
503
+ )
504
+ redirect_url_success: Annotated[
505
+ str,
506
+ Field(
507
+ alias="redirectUrlSuccess",
508
+ description="URL to redirect customers after successful payment.\n\nYou may include the literal `{CHECKOUT_ID}` placeholder anywhere in the\nURL (e.g. `https://example.com/return?checkout_id={CHECKOUT_ID}`). Vatly\nsubstitutes it with this checkout's id at creation time, so the host can\nlearn the checkout id on the redirect back without any server-side token\nstore. Only `{CHECKOUT_ID}` is supported.\n",
509
+ examples=["https://example.com/return?checkout_id={CHECKOUT_ID}"],
510
+ ),
511
+ ]
512
+ redirect_url_canceled: Annotated[
513
+ str,
514
+ Field(
515
+ alias="redirectUrlCanceled",
516
+ description="URL to redirect customers if they cancel the checkout.\n\nSupports the same `{CHECKOUT_ID}` placeholder as `redirectUrlSuccess`.\n",
517
+ examples=["https://example.com/canceled?checkout_id={CHECKOUT_ID}"],
518
+ ),
519
+ ]
520
+ customer_id: Annotated[
521
+ str | None,
522
+ Field(
523
+ alias="customerId",
524
+ description="Existing customer ID to associate with this checkout.\nIf provided, the customer's email will be pre-filled.\nMust match the testmode of the API token.\n",
525
+ examples=["customer_7kBmRtPvXw2NjLhYcZaE"],
526
+ ),
527
+ ] = None
528
+ billing_address: Annotated[
529
+ BillingAddress | None,
530
+ Field(
531
+ alias="billingAddress",
532
+ description="Optional editable prefill for the billing address fields on the hosted\ncheckout. When `country` is supplied, it takes precedence over the\nshopper's IP address for determining their country. A supplied `taxId`\nis only a prefill; it is not registered until the buyer submits the\ncheckout.\n",
533
+ ),
534
+ ] = None
535
+ metadata: Metadata | None = None
536
+ locale: Annotated[
537
+ str | None,
538
+ Field(
539
+ description="Language to present the hosted checkout in, including its validation and\nerror messages. Set this when you already know the customer's language —\nit is a better signal than their browser's.\n\nSend a bare language code (`de`), a BCP 47 tag (`de-AT`), or a POSIX /\nISO 15897 locale (`de_DE`) — whichever your stack already stores. All\nthree are folded to the language, so `de`, `de-AT` and `de_DE` are the\nsame request. There are no region-specific variants: an Austrian and a\nGerman customer are both served `de`.\n\nThe language also carries through to the payment provider's own hosted\npage, in whatever format that provider expects.\n\nSupported languages: `en`, `de`, `fr`, `nl`, `es`, `it`, `pt`, `pl`.\nAnything else is a `422` naming the supported set.\n\nOmit it (or send `null`) to detect the language from the shopper's\n`Accept-Language` header, falling back to English. A shopper can always\noverride your choice with the checkout's own language switcher.\n\nThe response echoes back the folded language, not the string you sent.\n",
540
+ examples=["de"],
541
+ ),
542
+ ] = None
543
+ products: Annotated[
544
+ Sequence[CheckoutProduct],
545
+ Field(
546
+ description="Products to include in this checkout",
547
+ examples=[
548
+ [
549
+ {"id": "one_off_product_Vr8kQdFhSrG4Y3DnfsdqH", "quantity": 1},
550
+ {"id": "subscription_plan_Rk5pQrSvWm8NjLhYbUcP", "trialDays": 14},
551
+ ]
552
+ ],
553
+ min_length=1,
554
+ ),
555
+ ]
556
+
557
+
558
+ class Links1(VatlyModel):
559
+ """
560
+ HATEOAS links to related resources
561
+ """
562
+
563
+ model_config = ConfigDict(
564
+ extra="allow",
565
+ populate_by_name=True,
566
+ )
567
+ self: Annotated[Link, Field(description="Link to this customer resource")]
568
+
569
+
570
+ class Customer(VatlyModel):
571
+ """
572
+ A customer represents a person or organization that can make purchases.
573
+ Email addresses are scoped to a storefront and testmode, so the same
574
+ address exists as separate customers in test and live mode.
575
+
576
+ """
577
+
578
+ model_config = ConfigDict(
579
+ extra="allow",
580
+ populate_by_name=True,
581
+ )
582
+ id: Annotated[
583
+ str,
584
+ Field(
585
+ description="Unique identifier for the customer",
586
+ examples=["customer_7kBmRtPvXw2NjLhYcZaE"],
587
+ ),
588
+ ]
589
+ resource: Annotated[
590
+ Literal["customer"],
591
+ Field(description="Resource type identifier", examples=["customer"]),
592
+ ]
593
+ testmode: Annotated[
594
+ bool,
595
+ Field(description="Whether this customer is in test mode", examples=[False]),
596
+ ]
597
+ email: Annotated[
598
+ str,
599
+ Field(
600
+ description="Customer's email address.\nInternationalized domains are stored and returned in their\nPunycode (ASCII) form per UTS #46 — e.g. `user@xn--mller-kva.de`.\nConvert back to Unicode for display in your own UI.\n",
601
+ examples=["customer@example.com"],
602
+ ),
603
+ ]
604
+ name: Annotated[
605
+ str | None,
606
+ Field(
607
+ description="Customer's display / account-holder name. This is an identity field\nfor communication (dunning emails, dashboard) — it is distinct from,\nand does not affect, the billing name on invoices.\n",
608
+ examples=["John Doe"],
609
+ ),
610
+ ] = None
611
+ created_at: Annotated[
612
+ AwareDatetime,
613
+ Field(
614
+ alias="createdAt",
615
+ description="When this customer was created (ISO 8601 format)",
616
+ examples=["2024-01-15T10:30:00Z"],
617
+ ),
618
+ ]
619
+ metadata: Metadata | None = None
620
+ links: Annotated[Links1, Field(description="HATEOAS links to related resources")]
621
+
622
+
623
+ class CreateCustomerRequest(VatlyModel):
624
+ """
625
+ Request body for creating a new customer
626
+ """
627
+
628
+ model_config = ConfigDict(
629
+ extra="allow",
630
+ populate_by_name=True,
631
+ )
632
+ email: Annotated[
633
+ str,
634
+ Field(
635
+ description="Customer's email address. Submit it in any form: the local part is\nlower-cased and an internationalized domain is converted to its\nPunycode (ASCII) form per UTS #46, so `User@Müller.de` is stored and\nreturned as `user@xn--mller-kva.de`.\n\nAn address another customer in this storefront and testmode already\nholds is not an error — that customer is returned with `200`.\n",
636
+ examples=["customer@example.com"],
637
+ ),
638
+ ]
639
+ name: Annotated[
640
+ str | None,
641
+ Field(
642
+ description="Customer's display / account-holder name.",
643
+ examples=["John Doe"],
644
+ max_length=255,
645
+ ),
646
+ ] = None
647
+ metadata: Metadata | None = None
648
+
649
+
650
+ class PendingUpdates(VatlyModel):
651
+ """
652
+ The changes that will take effect once a submitted update is approved, or
653
+ `null` when there is no pending update. Only the fields that actually
654
+ differ from the live product are present.
655
+
656
+ """
657
+
658
+ model_config = ConfigDict(
659
+ extra="allow",
660
+ populate_by_name=True,
661
+ )
662
+ name: Annotated[str | None, Field(examples=["Premium License v2"])] = None
663
+ description: Annotated[str | None, Field(examples=["Now with more premium"])] = None
664
+ base_price: Annotated[Money | None, Field(alias="basePrice")] = None
665
+ product_type: Annotated[Literal["saas", "ebook"] | None, Field(alias="productType")] = None
666
+
667
+
668
+ class Links2(VatlyModel):
669
+ """
670
+ HATEOAS links to related resources
671
+ """
672
+
673
+ model_config = ConfigDict(
674
+ extra="allow",
675
+ populate_by_name=True,
676
+ )
677
+ self: Annotated[Link, Field(description="Link to this product resource")]
678
+
679
+
680
+ class OneOffProduct(VatlyModel):
681
+ """
682
+ A one-off product represents a single-purchase item.
683
+ Products are configured in the Vatly dashboard and can be added to checkouts.
684
+
685
+ """
686
+
687
+ model_config = ConfigDict(
688
+ extra="allow",
689
+ populate_by_name=True,
690
+ )
691
+ id: Annotated[
692
+ str,
693
+ Field(
694
+ description="Unique identifier for the product (always starts with `one_off_product_`)",
695
+ examples=["one_off_product_Vr8kQdFhSrG4Y3DnfsdqH"],
696
+ pattern="^one_off_product_",
697
+ ),
698
+ ]
699
+ resource: Annotated[
700
+ Literal["one_off_product"],
701
+ Field(description="Resource type identifier", examples=["one_off_product"]),
702
+ ]
703
+ testmode: Annotated[
704
+ bool,
705
+ Field(description="Whether this product is in test mode", examples=[False]),
706
+ ]
707
+ name: Annotated[
708
+ str,
709
+ Field(description="Display name of the product", examples=["Premium License"]),
710
+ ]
711
+ description: Annotated[
712
+ str,
713
+ Field(
714
+ description="Detailed description of the product",
715
+ examples=["Lifetime access to all premium features"],
716
+ ),
717
+ ]
718
+ base_price: Annotated[
719
+ Money,
720
+ Field(
721
+ alias="basePrice",
722
+ description="Default price of the product (can be overridden in checkout).\nInterpretation depends on `taxBehavior`:\n- `exclusive` → `basePrice` is the net amount; tax is added at checkout.\n- `inclusive` → `basePrice` already includes tax; net is back-computed from the buyer's jurisdiction.\n",
723
+ ),
724
+ ]
725
+ tax_behavior: Annotated[
726
+ Literal["exclusive", "inclusive"],
727
+ Field(
728
+ alias="taxBehavior",
729
+ description="Whether `basePrice` is tax-exclusive (B2B convention; tax added on top at checkout)\nor tax-inclusive (B2C convention; price already includes tax).\nImmutable after product creation. A checkout may not mix products with different\n`taxBehavior` values; doing so returns `422 Unprocessable Entity`.\n",
730
+ examples=["exclusive"],
731
+ ),
732
+ ]
733
+ product_type: Annotated[
734
+ str,
735
+ Field(
736
+ alias="productType",
737
+ description="What kind of product this is:\n- `saas` — software supplied as a service (hosted, licensed or otherwise\n delivered online)\n- `ebook` — an electronic publication supplied as a download or stream\n\nSet when the product is created. Subscription plans accept `saas` only;\nan e-book is a one-time purchase, so sell it as a one-off product.\n",
738
+ examples=["saas"],
739
+ ),
740
+ ]
741
+ status: Annotated[
742
+ Literal["active", "pending", "rejected"],
743
+ Field(
744
+ description="Current status of the product:\n- `active` - Product is active and can be purchased\n- `pending` - Product is awaiting approval\n- `rejected` - Product has been rejected\n",
745
+ examples=["active"],
746
+ ),
747
+ ]
748
+ archived_at: Annotated[
749
+ AwareDatetime | None,
750
+ Field(
751
+ alias="archivedAt",
752
+ description="When this product was archived (ISO 8601), or `null` while it is on\nsale. Always present. An archived product is hidden from\n`GET /v1/one-off-products` and refused by `POST /v1/checkouts`; restore\nit with `DELETE /v1/one-off-products/{oneOffProductId}/archive`.\n",
753
+ examples=[None],
754
+ ),
755
+ ] = None
756
+ pending_updates: Annotated[
757
+ PendingUpdates | None,
758
+ Field(
759
+ alias="pendingUpdates",
760
+ description="The changes that will take effect once a submitted update is approved, or\n`null` when there is no pending update. Only the fields that actually\ndiffer from the live product are present.\n",
761
+ examples=[{"basePrice": {"currency": "EUR", "value": "349.00"}}],
762
+ ),
763
+ ] = None
764
+ update_status: Annotated[
765
+ Literal["pending", "reviewing"] | None,
766
+ Field(
767
+ alias="updateStatus",
768
+ description="Lifecycle of a pending update, or `null` when there is none:\n- `pending` - An update was submitted and is awaiting review\n- `reviewing` - The update is being reviewed\n",
769
+ examples=[None],
770
+ ),
771
+ ] = None
772
+ created_at: Annotated[
773
+ AwareDatetime,
774
+ Field(
775
+ alias="createdAt",
776
+ description="When this product was created (ISO 8601 format)",
777
+ examples=["2024-01-15T10:30:00Z"],
778
+ ),
779
+ ]
780
+ links: Annotated[Links2, Field(description="HATEOAS links to related resources")]
781
+
782
+
783
+ class CreateOneOffProductRequest(VatlyModel):
784
+ """
785
+ Request body for creating a one-off product.
786
+ """
787
+
788
+ model_config = ConfigDict(
789
+ extra="allow",
790
+ populate_by_name=True,
791
+ )
792
+ name: Annotated[
793
+ str,
794
+ Field(
795
+ description="Display name of the product.",
796
+ examples=["Premium License"],
797
+ max_length=255,
798
+ min_length=3,
799
+ ),
800
+ ]
801
+ description: Annotated[
802
+ str,
803
+ Field(
804
+ description="Detailed description of the product.",
805
+ examples=["Lifetime access to all premium features"],
806
+ ),
807
+ ]
808
+ base_price: Annotated[
809
+ EurMoney, Field(alias="basePrice", description="Price of the product, in EUR.")
810
+ ]
811
+ product_type: Annotated[
812
+ Literal["saas", "ebook"],
813
+ Field(
814
+ alias="productType",
815
+ description="What kind of product this is:\n- `saas` — software supplied as a service (hosted, licensed or otherwise\n delivered online)\n- `ebook` — an electronic publication supplied as a download or stream\n\nSet when the product is created. Subscription plans accept `saas` only;\nan e-book is a one-time purchase, so sell it as a one-off product.\n",
816
+ examples=["saas"],
817
+ ),
818
+ ]
819
+ tax_behavior: Annotated[
820
+ Literal["exclusive", "inclusive"] | None,
821
+ Field(
822
+ alias="taxBehavior",
823
+ description="Whether `basePrice` is tax-exclusive (B2B convention) or tax-inclusive\n(B2C convention). Optional; defaults to `exclusive`. Immutable after\nproduct creation.\n",
824
+ examples=["exclusive"],
825
+ ),
826
+ ] = "exclusive"
827
+
828
+
829
+ class PendingUpdates1(VatlyModel):
830
+ """
831
+ The changes that will take effect once a submitted update is approved, or
832
+ `null` when there is no pending update. Only the fields that actually
833
+ differ from the live plan are present.
834
+
835
+ """
836
+
837
+ model_config = ConfigDict(
838
+ extra="allow",
839
+ populate_by_name=True,
840
+ )
841
+ name: Annotated[str | None, Field(examples=["Pro Weekly"])] = None
842
+ description: Annotated[str | None, Field(examples=["Billed weekly"])] = None
843
+ base_price: Annotated[Money | None, Field(alias="basePrice")] = None
844
+ interval: Literal["day", "week", "month", "year"] | None = None
845
+ interval_count: Annotated[int | None, Field(alias="intervalCount", ge=1)] = None
846
+ product_type: Annotated[Literal["saas"] | None, Field(alias="productType")] = None
847
+
848
+
849
+ class Links3(VatlyModel):
850
+ """
851
+ HATEOAS links to related resources
852
+ """
853
+
854
+ model_config = ConfigDict(
855
+ extra="allow",
856
+ populate_by_name=True,
857
+ )
858
+ self: Annotated[Link, Field(description="Link to this plan resource")]
859
+
860
+
861
+ class SubscriptionPlan(VatlyModel):
862
+ """
863
+ A subscription plan defines recurring billing terms.
864
+ Plans are configured in the Vatly dashboard and can be added to checkouts.
865
+
866
+ """
867
+
868
+ model_config = ConfigDict(
869
+ extra="allow",
870
+ populate_by_name=True,
871
+ )
872
+ id: Annotated[
873
+ str,
874
+ Field(
875
+ description="Unique identifier for the plan (always starts with `subscription_plan_`)",
876
+ examples=["subscription_plan_Wt5mNvBxKw7YcZaEjLhR"],
877
+ pattern="^subscription_plan_",
878
+ ),
879
+ ]
880
+ resource: Annotated[
881
+ Literal["subscription_plan"],
882
+ Field(description="Resource type identifier", examples=["subscription_plan"]),
883
+ ]
884
+ testmode: Annotated[
885
+ bool, Field(description="Whether this plan is in test mode", examples=[False])
886
+ ]
887
+ name: Annotated[str, Field(description="Display name of the plan", examples=["Pro Monthly"])]
888
+ description: Annotated[
889
+ str,
890
+ Field(
891
+ description="Detailed description of the plan",
892
+ examples=["Full access to all Pro features, billed monthly"],
893
+ ),
894
+ ]
895
+ base_price: Annotated[
896
+ Money,
897
+ Field(
898
+ alias="basePrice",
899
+ description="Price per billing interval.\nInterpretation depends on `taxBehavior`:\n- `exclusive` → `basePrice` is the net amount; tax is added at checkout.\n- `inclusive` → `basePrice` already includes tax; net is back-computed from the buyer's jurisdiction.\n",
900
+ ),
901
+ ]
902
+ tax_behavior: Annotated[
903
+ Literal["exclusive", "inclusive"],
904
+ Field(
905
+ alias="taxBehavior",
906
+ description="Whether `basePrice` is tax-exclusive (B2B convention) or tax-inclusive (B2C convention).\nImmutable after plan creation — create a new plan for the other mode. A checkout may not\nmix products with different `taxBehavior` values; doing so returns\n`422 Unprocessable Entity`.\n",
907
+ examples=["exclusive"],
908
+ ),
909
+ ]
910
+ interval: Annotated[
911
+ Literal["day", "week", "month", "year"],
912
+ Field(
913
+ description="Billing interval unit:\n- `day` - Daily billing\n- `week` - Weekly billing\n- `month` - Monthly billing\n- `year` - Yearly billing\n",
914
+ examples=["month"],
915
+ ),
916
+ ]
917
+ interval_count: Annotated[
918
+ int,
919
+ Field(
920
+ alias="intervalCount",
921
+ description="Number of interval units between billing cycles.\nFor example, `interval: month` with `intervalCount: 3` bills every 3 months.\n",
922
+ examples=[1],
923
+ ge=1,
924
+ ),
925
+ ]
926
+ product_type: Annotated[
927
+ Literal["saas"],
928
+ Field(
929
+ alias="productType",
930
+ description="What kind of product this plan sells. Only `saas` is available on a\nsubscription plan — an e-book is a one-time purchase, so sell it as a\none-off product instead.\n\nSet when the plan is created.\n",
931
+ examples=["saas"],
932
+ ),
933
+ ]
934
+ status: Annotated[
935
+ Literal["active", "pending", "rejected"],
936
+ Field(
937
+ description="Current status of the plan:\n- `active` - Plan is active and can be subscribed to\n- `pending` - Plan is awaiting approval\n- `rejected` - Plan has been rejected\n",
938
+ examples=["active"],
939
+ ),
940
+ ]
941
+ archived_at: Annotated[
942
+ AwareDatetime | None,
943
+ Field(
944
+ alias="archivedAt",
945
+ description="When this plan was archived (ISO 8601), or `null` while it is open to\nnew business. Always present. An archived plan is hidden from\n`GET /v1/subscription-plans`, refused by `POST /v1/checkouts`, and\ncannot be switched to from `PATCH /v1/subscriptions/{subscriptionId}`.\nSubscribers already on the plan keep billing unchanged. Restore it with\n`DELETE /v1/subscription-plans/{subscriptionPlanId}/archive`.\n",
946
+ examples=[None],
947
+ ),
948
+ ] = None
949
+ pending_updates: Annotated[
950
+ PendingUpdates1 | None,
951
+ Field(
952
+ alias="pendingUpdates",
953
+ description="The changes that will take effect once a submitted update is approved, or\n`null` when there is no pending update. Only the fields that actually\ndiffer from the live plan are present.\n",
954
+ examples=[{"interval": "week", "intervalCount": 1}],
955
+ ),
956
+ ] = None
957
+ update_status: Annotated[
958
+ Literal["pending", "reviewing"] | None,
959
+ Field(
960
+ alias="updateStatus",
961
+ description="Lifecycle of a pending update, or `null` when there is none:\n- `pending` - An update was submitted and is awaiting review\n- `reviewing` - The update is being reviewed\n",
962
+ examples=[None],
963
+ ),
964
+ ] = None
965
+ created_at: Annotated[
966
+ AwareDatetime,
967
+ Field(
968
+ alias="createdAt",
969
+ description="When this plan was created (ISO 8601 format)",
970
+ examples=["2024-01-15T10:30:00Z"],
971
+ ),
972
+ ]
973
+ links: Annotated[Links3, Field(description="HATEOAS links to related resources")]
974
+
975
+
976
+ class CreateSubscriptionPlanRequest(VatlyModel):
977
+ """
978
+ Request body for creating a subscription plan.
979
+ """
980
+
981
+ model_config = ConfigDict(
982
+ extra="allow",
983
+ populate_by_name=True,
984
+ )
985
+ name: Annotated[
986
+ str,
987
+ Field(
988
+ description="Display name of the plan.",
989
+ examples=["Pro Monthly"],
990
+ max_length=255,
991
+ min_length=3,
992
+ ),
993
+ ]
994
+ description: Annotated[
995
+ str,
996
+ Field(
997
+ description="Detailed description of the plan.",
998
+ examples=["Full access to all Pro features, billed monthly"],
999
+ ),
1000
+ ]
1001
+ base_price: Annotated[
1002
+ EurMoney,
1003
+ Field(alias="basePrice", description="Price per billing interval, in EUR."),
1004
+ ]
1005
+ product_type: Annotated[
1006
+ Literal["saas"],
1007
+ Field(
1008
+ alias="productType",
1009
+ description="What kind of product this plan sells. Only `saas` is available on a\nsubscription plan — an e-book is a one-time purchase, so sell it as a\none-off product instead.\n\nSet when the plan is created.\n",
1010
+ examples=["saas"],
1011
+ ),
1012
+ ]
1013
+ tax_behavior: Annotated[
1014
+ Literal["exclusive", "inclusive"] | None,
1015
+ Field(
1016
+ alias="taxBehavior",
1017
+ description="Whether `basePrice` is tax-exclusive (B2B convention) or tax-inclusive\n(B2C convention). Optional; defaults to `exclusive`. Immutable after\nplan creation — create a new plan for the other mode.\n",
1018
+ examples=["exclusive"],
1019
+ ),
1020
+ ] = "exclusive"
1021
+ interval: Annotated[
1022
+ Literal["day", "week", "month", "year"],
1023
+ Field(
1024
+ description="Billing interval unit. `day` is sandbox-only (test mode); live plans\nsupport `week`, `month`, and `year`.\n",
1025
+ examples=["month"],
1026
+ ),
1027
+ ]
1028
+ interval_count: Annotated[
1029
+ int,
1030
+ Field(
1031
+ alias="intervalCount",
1032
+ description="Number of interval units between billing cycles. Bounded per unit:\nup to 365 days, 52 weeks, or 12 months. For `year`, billing is always\nannual and this value is ignored.\n",
1033
+ examples=[1],
1034
+ ge=1,
1035
+ ),
1036
+ ]
1037
+
1038
+
1039
+ class Links4(VatlyModel):
1040
+ """
1041
+ HATEOAS links to related resources
1042
+ """
1043
+
1044
+ model_config = ConfigDict(
1045
+ extra="allow",
1046
+ populate_by_name=True,
1047
+ )
1048
+ self: Annotated[Link, Field(description="Link to this order resource")]
1049
+ customer: Annotated[
1050
+ Link | None,
1051
+ Field(
1052
+ description="Link to the customer who made this purchase. Null for orders with no associated customer."
1053
+ ),
1054
+ ] = None
1055
+ customer_invoice: Annotated[
1056
+ Link,
1057
+ Field(
1058
+ alias="customerInvoice",
1059
+ description="Link to the customer-facing invoice page in the customer portal (text/html).\nOn a refund or chargeback order this page is the credit note. The link is signed\nand expires after 72 hours; an expired link lets the customer request a fresh one by email.\n",
1060
+ ),
1061
+ ]
1062
+
1063
+
1064
+ class InvoiceUpdateLink(VatlyModel):
1065
+ """
1066
+ A signed URL for the customer to update invoice details (billing address, VAT number, company name)
1067
+ """
1068
+
1069
+ model_config = ConfigDict(
1070
+ extra="allow",
1071
+ populate_by_name=True,
1072
+ )
1073
+ href: Annotated[
1074
+ str,
1075
+ Field(
1076
+ description="Signed URL to the address update page.\nValid for a limited time (typically 24 hours).\n",
1077
+ examples=[
1078
+ "https://vatly.com/invoices/order_Hn5xWqVfKm8RjTgYbUcP/edit?signature=abc123..."
1079
+ ],
1080
+ ),
1081
+ ]
1082
+ type: Annotated[
1083
+ Literal["text/html"],
1084
+ Field(description="Content type of the linked page", examples=["text/html"]),
1085
+ ]
1086
+
1087
+
1088
+ class Links5(VatlyModel):
1089
+ """
1090
+ HATEOAS links to related resources
1091
+ """
1092
+
1093
+ model_config = ConfigDict(
1094
+ extra="allow",
1095
+ populate_by_name=True,
1096
+ )
1097
+ self: Annotated[Link, Field(description="Link to this refund resource")]
1098
+ original_order: Annotated[
1099
+ Link, Field(alias="originalOrder", description="Link to the original order")
1100
+ ]
1101
+ order: Annotated[
1102
+ Link | None,
1103
+ Field(
1104
+ description="Link to the credit note order. Always present; `null` until the credit-note order is created."
1105
+ ),
1106
+ ] = None
1107
+ customer_invoice: Annotated[
1108
+ Link | None,
1109
+ Field(
1110
+ alias="customerInvoice",
1111
+ description="Link to this refund's credit note in the customer portal (text/html). This is the\nsame page as the credit-note order's `customerInvoice` link, not the original order's invoice.\nAlways present; `null` until the refund is processed. The link is signed and\nexpires after 72 hours; an expired link lets the customer request a fresh one by email.\n",
1112
+ ),
1113
+ ] = None
1114
+
1115
+
1116
+ class CreateFullRefundRequest(VatlyModel):
1117
+ """
1118
+ Request body for creating a full refund
1119
+ """
1120
+
1121
+ model_config = ConfigDict(
1122
+ extra="allow",
1123
+ populate_by_name=True,
1124
+ )
1125
+ metadata: Metadata | None = None
1126
+
1127
+
1128
+ class Links6(VatlyModel):
1129
+ """
1130
+ HATEOAS links to related resources
1131
+ """
1132
+
1133
+ model_config = ConfigDict(
1134
+ extra="allow",
1135
+ populate_by_name=True,
1136
+ )
1137
+ self: Annotated[Link, Field(description="Link to this chargeback resource")]
1138
+ original_order: Annotated[
1139
+ Link, Field(alias="originalOrder", description="Link to the original order")
1140
+ ]
1141
+ order: Annotated[
1142
+ Link | None,
1143
+ Field(
1144
+ description="Link to the credit note order. Always present; `null` until the credit-note order is created."
1145
+ ),
1146
+ ] = None
1147
+ customer_invoice: Annotated[
1148
+ Link | None,
1149
+ Field(
1150
+ alias="customerInvoice",
1151
+ description="Link to this chargeback's credit note in the customer portal (text/html). This is the\nsame page as the credit-note order's `customerInvoice` link, not the original order's invoice.\nAlways present; `null` until the lost chargeback is processed. The link is signed and\nexpires after 72 hours; an expired link lets the customer request a fresh one by email.\n",
1152
+ ),
1153
+ ] = None
1154
+
1155
+
1156
+ class Links7(VatlyModel):
1157
+ """
1158
+ HATEOAS links to related resources. Always present on the persisted event.
1159
+ """
1160
+
1161
+ model_config = ConfigDict(
1162
+ extra="allow",
1163
+ populate_by_name=True,
1164
+ )
1165
+ self: Annotated[Link, Field(description="Link to this webhook event resource")]
1166
+
1167
+
1168
+ class Links8(VatlyModel):
1169
+ """
1170
+ HATEOAS links to related resources
1171
+ """
1172
+
1173
+ model_config = ConfigDict(
1174
+ extra="allow",
1175
+ populate_by_name=True,
1176
+ )
1177
+ self: Annotated[Link, Field(description="Link to this webhook endpoint resource")]
1178
+
1179
+
1180
+ class WebhookEndpoint(VatlyModel):
1181
+ """
1182
+ A registered webhook endpoint. Vatly POSTs event deliveries to its `url`
1183
+ and signs each one with the secret you set (see the `Vatly-Signature`
1184
+ header).
1185
+
1186
+ A storefront can have up to five endpoints in each mode. URLs must be
1187
+ unique within the storefront and mode. Each endpoint receives only the
1188
+ public event names in `enabledEvents`; an empty list makes it dormant.
1189
+
1190
+ The signing secret is write-only: you provide it on creation (and may rotate
1191
+ it via update) but it is never returned in any response. Store the value you
1192
+ send; the API will not give it back.
1193
+
1194
+ """
1195
+
1196
+ model_config = ConfigDict(
1197
+ extra="allow",
1198
+ populate_by_name=True,
1199
+ )
1200
+ id: Annotated[
1201
+ str,
1202
+ Field(
1203
+ description="Unique identifier for the webhook endpoint",
1204
+ examples=["webhook_QdEpFhdSrG4Y3DnfsdqsH"],
1205
+ ),
1206
+ ]
1207
+ resource: Annotated[
1208
+ Literal["webhook_endpoint"],
1209
+ Field(description="Resource type identifier", examples=["webhook_endpoint"]),
1210
+ ]
1211
+ testmode: Annotated[
1212
+ bool,
1213
+ Field(
1214
+ description="Whether this endpoint receives test-mode events",
1215
+ examples=[False],
1216
+ ),
1217
+ ]
1218
+ url: Annotated[
1219
+ str,
1220
+ Field(
1221
+ description="The HTTPS URL deliveries are POSTed to",
1222
+ examples=["https://merchant.example/webhooks/vatly"],
1223
+ ),
1224
+ ]
1225
+ enabled_events: Annotated[
1226
+ Sequence[str],
1227
+ Field(
1228
+ alias="enabledEvents",
1229
+ description="The endpoint's persisted subscription names. Current names in this list\nare delivered; a retired public name can remain visible for stable\nreadback but is inert. New event names are not added automatically;\nupdate this list to opt in. An empty list means no domain events are\ndelivered, although `webhook.setup` is still sent whenever Vatly\nverifies the endpoint configuration.\n",
1230
+ examples=[["order.paid", "refund.completed"]],
1231
+ ),
1232
+ ]
1233
+ created_at: Annotated[
1234
+ AwareDatetime,
1235
+ Field(
1236
+ alias="createdAt",
1237
+ description="When this endpoint was created (ISO 8601 format)",
1238
+ examples=["2024-01-15T10:30:00Z"],
1239
+ ),
1240
+ ]
1241
+ links: Annotated[Links8, Field(description="HATEOAS links to related resources")]
1242
+
1243
+
1244
+ class Links9(VatlyModel):
1245
+ """
1246
+ HATEOAS links to related resources
1247
+ """
1248
+
1249
+ model_config = ConfigDict(
1250
+ extra="allow",
1251
+ populate_by_name=True,
1252
+ )
1253
+ self: Annotated[Link, Field(description="Link to this subscription resource")]
1254
+ customer: Annotated[Link, Field(description="Link to the customer who owns this subscription")]
1255
+
1256
+
1257
+ class ScheduledUpdate(VatlyModel):
1258
+ """
1259
+ The target values for a change scheduled to take effect at the subscription's next
1260
+ billing cycle — set by `PATCH /subscriptions/{subscriptionId}` with
1261
+ `applyImmediately: false`. The rest of the object is the subscription's *current*
1262
+ state; `scheduledUpdate` is what it will become.
1263
+
1264
+ Null when nothing is pending. Always present on both the REST resource and webhook
1265
+ deliveries, so this is the authoritative way to reconcile a pending change — do not
1266
+ depend on having received the `subscription.update_scheduled` webhook, which is
1267
+ delivered once.
1268
+
1269
+ Cleared when the change is applied at renewal, and when it is discarded because the
1270
+ subscription was canceled before that renewal happened.
1271
+
1272
+ """
1273
+
1274
+ model_config = ConfigDict(
1275
+ extra="allow",
1276
+ populate_by_name=True,
1277
+ )
1278
+ subscription_plan_id: Annotated[
1279
+ str,
1280
+ Field(
1281
+ alias="subscriptionPlanId",
1282
+ description="ID of the plan the subscription will switch to",
1283
+ examples=["subscription_plan_Wt5mNvBxKw7YcZaEjLhR"],
1284
+ ),
1285
+ ]
1286
+ name: Annotated[str, Field(description="Name of the scheduled plan", examples=["Pro Annual"])]
1287
+ description: Annotated[str, Field(description="Description of the scheduled plan")]
1288
+ base_price: Annotated[
1289
+ Money,
1290
+ Field(
1291
+ alias="basePrice",
1292
+ description="Scheduled price per billing cycle before taxes",
1293
+ ),
1294
+ ]
1295
+ quantity: Annotated[int, Field(description="Scheduled quantity", examples=[1], ge=1)]
1296
+ interval: Annotated[
1297
+ Literal["day", "week", "month", "year"],
1298
+ Field(description="Scheduled billing interval unit", examples=["month"]),
1299
+ ]
1300
+ interval_count: Annotated[
1301
+ int,
1302
+ Field(
1303
+ alias="intervalCount",
1304
+ description="Scheduled number of interval units between billing cycles",
1305
+ examples=[1],
1306
+ ge=1,
1307
+ ),
1308
+ ]
1309
+ effective_at: Annotated[
1310
+ AwareDatetime | None,
1311
+ Field(
1312
+ alias="effectiveAt",
1313
+ description="When the change takes effect — the subscription's next renewal date. Null if the\nsubscription has no scheduled renewal.\n",
1314
+ examples=["2024-03-15T10:30:00Z"],
1315
+ ),
1316
+ ] = None
1317
+
1318
+
1319
+ class Price(VatlyModel):
1320
+ """
1321
+ Change the recurring price, keeping the current plan. Provide
1322
+ `price` without a `subscriptionPlanId` to change the price in place;
1323
+ combine it with `subscriptionPlanId` to switch plans at a custom
1324
+ price. The currency must match the subscription's currency. The
1325
+ `prorate`, `applyImmediately`, `invoiceImmediately` and anchor
1326
+ options apply exactly as they do for a plan or quantity change.
1327
+
1328
+ """
1329
+
1330
+ model_config = ConfigDict(
1331
+ extra="allow",
1332
+ populate_by_name=True,
1333
+ )
1334
+ value: Annotated[
1335
+ str,
1336
+ Field(
1337
+ description="Price amount as a decimal string, with the exact number of decimals for the currency.",
1338
+ examples=["99.99"],
1339
+ ),
1340
+ ]
1341
+ currency: Annotated[
1342
+ Literal["EUR", "USD"],
1343
+ Field(
1344
+ description="Must match the subscription's own currency (a subscription's\ncurrency is fixed at checkout and cannot be changed via an update).\n",
1345
+ examples=["EUR"],
1346
+ ),
1347
+ ]
1348
+
1349
+
1350
+ class UpdateSubscriptionRequest(VatlyModel):
1351
+ """
1352
+ Request body for updating a subscription.
1353
+ At least one of `subscriptionPlanId`, `quantity`, or `price` must be provided.
1354
+
1355
+ """
1356
+
1357
+ model_config = ConfigDict(
1358
+ extra="allow",
1359
+ populate_by_name=True,
1360
+ )
1361
+ subscription_plan_id: Annotated[
1362
+ str | None,
1363
+ Field(
1364
+ alias="subscriptionPlanId",
1365
+ description="ID of the new subscription plan.\nMust match the testmode of the current subscription.\n",
1366
+ examples=["subscription_plan_Wt5mNvBxKw7YcZaEjLhR"],
1367
+ pattern="^subscription_plan_",
1368
+ ),
1369
+ ] = None
1370
+ quantity: Annotated[
1371
+ int | None,
1372
+ Field(
1373
+ description="The new total quantity (e.g. number of seats). This sets the\nquantity to the given value — it is not added to the current\nquantity. Omit it to leave the quantity unchanged.\n",
1374
+ examples=[5],
1375
+ ge=1,
1376
+ ),
1377
+ ] = None
1378
+ price: Annotated[
1379
+ Price | None,
1380
+ Field(
1381
+ description="Change the recurring price, keeping the current plan. Provide\n`price` without a `subscriptionPlanId` to change the price in place;\ncombine it with `subscriptionPlanId` to switch plans at a custom\nprice. The currency must match the subscription's currency. The\n`prorate`, `applyImmediately`, `invoiceImmediately` and anchor\noptions apply exactly as they do for a plan or quantity change.\n"
1382
+ ),
1383
+ ] = None
1384
+ prorate: Annotated[
1385
+ bool | None,
1386
+ Field(
1387
+ description="How an immediate change is priced.\n- `true`: the new price for the time left on the cycle, minus a credit\n for the old price over that same time\n- `false`: the full new price for the current cycle, on top of what was\n already paid. This does not skip the charge; use\n `prorationCharge: false` for that\n\nIgnored when `prorationCharge` is `false`.\n",
1388
+ examples=[True],
1389
+ ),
1390
+ ] = True
1391
+ proration_charge: Annotated[
1392
+ bool | None,
1393
+ Field(
1394
+ alias="prorationCharge",
1395
+ description="Whether an immediate change is charged at all.\n- `true`: charged as set by `prorate` and `invoiceImmediately`\n- `false`: the change applies now and nothing is charged for it. The\n next renewal bills the new price, unless a change is already\n scheduled for that renewal: that change still applies. A downgrade\n gets no credit. During a trial the trial keeps running and its end\n bills the new plan\n\nWith `false`, `invoiceImmediately: true`, `anchor`, `resetAnchor`,\n`trialUntil` and a change to a plan with a different interval are\nrejected with `422`: each would start or bill a new cycle now.\n",
1396
+ examples=[False],
1397
+ ),
1398
+ ] = True
1399
+ apply_immediately: Annotated[
1400
+ bool | None,
1401
+ Field(
1402
+ alias="applyImmediately",
1403
+ description="Whether to apply changes immediately or at the end of the current billing period.\n- `true`: Changes take effect immediately (with proration if enabled)\n- `false`: Changes apply when the current period ends\n\nDuring a trial, `true` ends the trial and starts the first paid cycle,\ncharged right away. Without `anchor` the cycle starts that day at the\nfull new price; with `anchor` it runs to that date, priced per\n`prorate`. A subscription that is `on_grace_period` starts no paid\ncycle and still ends on its communicated date.\n",
1404
+ examples=[True],
1405
+ ),
1406
+ ] = False
1407
+ invoice_immediately: Annotated[
1408
+ bool | None,
1409
+ Field(
1410
+ alias="invoiceImmediately",
1411
+ description="Whether to generate and charge an invoice immediately for proration.\nOnly applies when `applyImmediately` and `prorate` are both true.\n\nWhen `false` (the default) the proration delta is parked on the current\ncycle and billed as a line on the next renewal invoice — the customer\ngets one invoice and one payment covering both. If the subscription is\ncancelled before that renewal there is no next invoice and the parked\namount is waived, so a customer is never charged at the moment they\ncancel.\n\n**Set this to `true` on yearly and other long-interval plans.** The\ndefault suits monthly billing, where the delta is collected within\nweeks. On a yearly plan an upgrade in month 2 is not billed until\nmonth 12, and is waived entirely if the customer cancels at any point\nin between — so the longer the interval, the more the deferral costs\nyou. Charging immediately trades that for a separate invoice and\npayment (and its transaction fee) per change.\n\nAn immediate update during a trial is always charged right away,\nwhatever this is set to, because it starts the first paid cycle. This\ndoes not apply to a subscription that is `on_grace_period`.\n",
1412
+ examples=[False],
1413
+ ),
1414
+ ] = False
1415
+ anchor: Annotated[
1416
+ date | None,
1417
+ Field(
1418
+ description="Set the billing anchor to a specific calendar date (YYYY-MM-DD,\ninterpreted in UTC). The new billing cycle is recalculated around\nthis anchor. Date-only by design — the anchor identifies a day,\nnot an instant, so daylight-saving transitions don't shift it.\nCannot be combined with `resetAnchor`.\n",
1419
+ examples=["2024-02-01"],
1420
+ ),
1421
+ ] = None
1422
+ reset_anchor: Annotated[
1423
+ bool | None,
1424
+ Field(
1425
+ alias="resetAnchor",
1426
+ description='Reset the billing anchor to "now" — the moment the update\nis processed becomes the new cycle anchor, and future\nrenewals align to it. Useful when a customer upgrades\nmid-cycle and you want the new plan\'s billing period to\nstart fresh from today.\nCannot be combined with `anchor`.\n',
1427
+ examples=[True],
1428
+ ),
1429
+ ] = False
1430
+ trial_until: Annotated[
1431
+ AwareDatetime | None,
1432
+ Field(
1433
+ alias="trialUntil",
1434
+ description="Extend or set a trial period until this date (ISO 8601 format).\nCannot be combined with `anchor`.\n",
1435
+ examples=["2024-02-15T00:00:00Z"],
1436
+ ),
1437
+ ] = None
1438
+
1439
+
1440
+ class UpdateSubscriptionBillingRequest(VatlyModel):
1441
+ """
1442
+ Request body for updating subscription billing details via hosted flow
1443
+ """
1444
+
1445
+ model_config = ConfigDict(
1446
+ extra="allow",
1447
+ populate_by_name=True,
1448
+ )
1449
+ redirect_url_success: Annotated[
1450
+ str,
1451
+ Field(
1452
+ alias="redirectUrlSuccess",
1453
+ description="URL to redirect after successful billing update",
1454
+ examples=["https://example.com/billing-updated"],
1455
+ ),
1456
+ ]
1457
+ redirect_url_canceled: Annotated[
1458
+ str,
1459
+ Field(
1460
+ alias="redirectUrlCanceled",
1461
+ description="URL to redirect if customer cancels the update",
1462
+ examples=["https://example.com/billing-canceled"],
1463
+ ),
1464
+ ]
1465
+ billing_address: Annotated[
1466
+ BillingAddress | None,
1467
+ Field(
1468
+ alias="billingAddress",
1469
+ description="Optional editable prefill for the hosted billing update form. When\nsupplied, it replaces the subscription's stored billing details as the\nprefill, so send every field you want shown; empty strings give a blank\nform. Omit it, or send `null` or `{}`, to prefill the stored details.\n`country` must be an ISO 3166-1 alpha-2 code. A supplied `taxId` is\nonly a prefill; it is not registered until the customer submits the\nform.\n",
1470
+ ),
1471
+ ] = None
1472
+
1473
+
1474
+ class Mandate(VatlyModel):
1475
+ """
1476
+ A mandate represents an authorization to charge a customer's payment method
1477
+ on a recurring basis (cards, SEPA Direct Debit, PayPal, Bacs).
1478
+
1479
+ This shape is the inline summary exposed on subscriptions — enough to
1480
+ render "Visa ending in 4242" in a billing portal without an extra round
1481
+ trip. A dedicated `/v1/mandates/{id}` endpoint with richer detail (status,
1482
+ validity timestamps, billing snapshot) is on the roadmap; fields here are
1483
+ forward-compatible additions to that future schema.
1484
+
1485
+ """
1486
+
1487
+ model_config = ConfigDict(
1488
+ extra="allow",
1489
+ populate_by_name=True,
1490
+ )
1491
+ method: Annotated[
1492
+ Literal["card", "sepa_debit", "paypal", "bacs_debit"],
1493
+ Field(
1494
+ description="Normalized payment method category, provider-agnostic:\n- `card` — credit/debit card (incl. Apple Pay, Google Pay wallets)\n- `sepa_debit` — SEPA Direct Debit (incl. iDEAL, Bancontact, KBC and\n other rail aliases that settle into a directdebit mandate)\n- `paypal` — PayPal billing agreement\n- `bacs_debit` — UK Bacs Direct Debit\n",
1495
+ examples=["card"],
1496
+ ),
1497
+ ]
1498
+ masked_identifier: Annotated[
1499
+ str | None,
1500
+ Field(
1501
+ alias="maskedIdentifier",
1502
+ description='Customer-facing masked identifier for the payment method on file:\nthe last 4 of a card (`"4242"`), a masked IBAN (`"NL91****4300"`),\na masked PayPal email (`"j***@example.com"`), or last 4 of a Bacs\naccount (`"****5678"`).\n\nMay briefly be `null` for freshly-subscribed customers when the\nunderlying authorization event doesn\'t carry the masked identifier\ninline — in that case it lands on a follow-up sync (runs at most\nevery 5 minutes) and the field updates from `null` to the resolved\nvalue once the sync completes.\n',
1503
+ examples=["4242"],
1504
+ ),
1505
+ ] = None
1506
+
1507
+
1508
+ class UpdateCustomerRequest(VatlyModel):
1509
+ """
1510
+ Request body for updating a customer's identity fields. Only `name` and
1511
+ `email` may be changed here — both are optional (send either or both).
1512
+
1513
+ Billing-address fields (company name, tax ID, street, city, country, etc.)
1514
+ are not supported by this endpoint and are ignored; amend those through the
1515
+ hosted billing-update flow, which validates tax-relevant data centrally.
1516
+
1517
+ """
1518
+
1519
+ model_config = ConfigDict(
1520
+ extra="allow",
1521
+ populate_by_name=True,
1522
+ )
1523
+ name: Annotated[
1524
+ str | None,
1525
+ Field(
1526
+ description="Customer's display / account-holder name.",
1527
+ examples=["John Doe"],
1528
+ max_length=255,
1529
+ ),
1530
+ ] = None
1531
+ email: Annotated[
1532
+ str | None,
1533
+ Field(
1534
+ description="Customer's email address, canonicalized on the way in exactly as on\ncreate (lower-cased local part, Punycode domain per UTS #46).\n\nUnlike create, a collision is rejected here: moving this customer onto\nan address another customer in the same storefront and testmode holds\nreturns `422`. Creating with a known address is a recovery; moving one\ncustomer onto another's address is a conflict.\n",
1535
+ examples=["new.email@example.com"],
1536
+ ),
1537
+ ] = None
1538
+
1539
+
1540
+ class CreatePortalSessionRequest(VatlyModel):
1541
+ """
1542
+ Optional settings for a short-lived hosted customer portal session. The body
1543
+ may be omitted or sent as `{}`. `returnUrl` must be sent here in the JSON
1544
+ body; query-string input is rejected.
1545
+
1546
+ """
1547
+
1548
+ model_config = ConfigDict(
1549
+ extra="allow",
1550
+ populate_by_name=True,
1551
+ )
1552
+ return_url: Annotated[
1553
+ str | None,
1554
+ Field(
1555
+ alias="returnUrl",
1556
+ description="Absolute HTTPS URL without embedded credentials, rendered as a return\nlink in the hosted portal. The runtime limit is 2,048 UTF-8 bytes.\nVatly never fetches or automatically redirects to this URL.\n",
1557
+ examples=["https://merchant.example/account/billing"],
1558
+ max_length=2048,
1559
+ pattern="^[Hh][Tt][Tt][Pp][Ss]://",
1560
+ ),
1561
+ ] = None
1562
+
1563
+
1564
+ class PortalSession(VatlyModel):
1565
+ """
1566
+ A short-lived, single-use entry link to the hosted customer portal. Redirect
1567
+ the customer's browser to `url`; do not store it as a long-lived account
1568
+ link.
1569
+
1570
+ """
1571
+
1572
+ model_config = ConfigDict(
1573
+ extra="allow",
1574
+ populate_by_name=True,
1575
+ )
1576
+ url: Annotated[
1577
+ str,
1578
+ Field(
1579
+ description="Single-use HTTPS URL to redirect the customer to.",
1580
+ examples=["https://billing.vatly.com/authenticate?credential=..."],
1581
+ ),
1582
+ ]
1583
+ expires_at: Annotated[
1584
+ AwareDatetime,
1585
+ Field(
1586
+ alias="expiresAt",
1587
+ description="Expiry of the one-time entry link, not of the resulting browser session.",
1588
+ examples=["2026-09-01T12:15:00Z"],
1589
+ ),
1590
+ ]
1591
+ return_url: Annotated[
1592
+ str | None,
1593
+ Field(
1594
+ alias="returnUrl",
1595
+ description="The validated absolute HTTPS return URL supplied in the request, or\nnull. It contains no embedded credentials and is limited to 2,048\nUTF-8 bytes.\n",
1596
+ examples=["https://merchant.example/account/billing"],
1597
+ max_length=2048,
1598
+ pattern="^[Hh][Tt][Tt][Pp][Ss]://",
1599
+ ),
1600
+ ] = None
1601
+
1602
+
1603
+ class UpdateOneOffProductRequest(VatlyModel):
1604
+ """
1605
+ Request body for submitting an update to a live one-off product. Each request
1606
+ is the **complete set of changes** relative to the current live product, and
1607
+ must contain at least one field. A field submitted equal to the live value is
1608
+ ignored; a submission that nets to no change clears any pending update. The
1609
+ update enters review (`updateStatus: pending`) before it takes effect (in test
1610
+ mode it is approved automatically).
1611
+
1612
+ """
1613
+
1614
+ model_config = ConfigDict(
1615
+ extra="allow",
1616
+ populate_by_name=True,
1617
+ )
1618
+ name: Annotated[
1619
+ str | None,
1620
+ Field(
1621
+ description="New display name of the product.",
1622
+ examples=["Premium License v2"],
1623
+ max_length=255,
1624
+ min_length=3,
1625
+ ),
1626
+ ] = None
1627
+ description: Annotated[
1628
+ str | None,
1629
+ Field(
1630
+ description="New description of the product.",
1631
+ examples=["Now with more premium"],
1632
+ ),
1633
+ ] = None
1634
+ base_price: Annotated[
1635
+ EurMoney | None,
1636
+ Field(alias="basePrice", description="New price of the product, in EUR."),
1637
+ ] = None
1638
+ product_type: Annotated[
1639
+ Literal["saas", "ebook"] | None,
1640
+ Field(
1641
+ alias="productType",
1642
+ description="What kind of product this is:\n- `saas` — software supplied as a service (hosted, licensed or otherwise\n delivered online)\n- `ebook` — an electronic publication supplied as a download or stream\n\nSet when the product is created. Subscription plans accept `saas` only;\nan e-book is a one-time purchase, so sell it as a one-off product.\n",
1643
+ examples=["saas"],
1644
+ ),
1645
+ ] = None
1646
+
1647
+
1648
+ class UpdateSubscriptionPlanRequest(VatlyModel):
1649
+ """
1650
+ Request body for submitting an update to a live subscription plan. Each
1651
+ request is the **complete set of changes** relative to the current live plan,
1652
+ and must contain at least one field. A field submitted equal to the live value
1653
+ is ignored; a submission that nets to no change clears any pending update. The
1654
+ update enters review (`updateStatus: pending`) before it takes effect (in test
1655
+ mode it is approved automatically).
1656
+
1657
+ The interval cannot be changed once the plan has ever been used by a
1658
+ subscription, active or not. The price stays changeable.
1659
+
1660
+ """
1661
+
1662
+ model_config = ConfigDict(
1663
+ extra="allow",
1664
+ populate_by_name=True,
1665
+ )
1666
+ name: Annotated[
1667
+ str | None,
1668
+ Field(
1669
+ description="New display name of the plan.",
1670
+ examples=["Pro Weekly"],
1671
+ max_length=255,
1672
+ min_length=3,
1673
+ ),
1674
+ ] = None
1675
+ description: Annotated[
1676
+ str | None,
1677
+ Field(description="New description of the plan.", examples=["Billed weekly"]),
1678
+ ] = None
1679
+ base_price: Annotated[
1680
+ EurMoney | None,
1681
+ Field(alias="basePrice", description="New price per billing interval, in EUR."),
1682
+ ] = None
1683
+ product_type: Annotated[
1684
+ Literal["saas"] | None, Field(alias="productType", examples=["saas"])
1685
+ ] = None
1686
+ interval: Annotated[
1687
+ Literal["day", "week", "month", "year"] | None, Field(examples=["week"])
1688
+ ] = None
1689
+ interval_count: Annotated[
1690
+ int | None,
1691
+ Field(
1692
+ alias="intervalCount",
1693
+ description="Required when `interval` is provided.",
1694
+ examples=[1],
1695
+ ge=1,
1696
+ ),
1697
+ ] = None
1698
+
1699
+
1700
+ class TaxSummaryRate(VatlyModel):
1701
+ """
1702
+ Simplified tax rate information
1703
+ """
1704
+
1705
+ model_config = ConfigDict(
1706
+ extra="allow",
1707
+ populate_by_name=True,
1708
+ )
1709
+ name: Annotated[str, Field(description="Human-readable name of the tax rate", examples=["VAT"])]
1710
+ percentage: Annotated[float, Field(description="Tax rate percentage", examples=[21])]
1711
+ taxable_percentage: Annotated[
1712
+ float,
1713
+ Field(
1714
+ alias="taxablePercentage",
1715
+ description="Percentage of the amount that is taxable",
1716
+ examples=[100],
1717
+ ),
1718
+ ]
1719
+
1720
+
1721
+ class TaxSummaryItem(VatlyModel):
1722
+ """
1723
+ A single tax rate entry with its calculated amount
1724
+ """
1725
+
1726
+ model_config = ConfigDict(
1727
+ extra="allow",
1728
+ populate_by_name=True,
1729
+ )
1730
+ tax_rate: Annotated[TaxSummaryRate, Field(alias="taxRate")]
1731
+ amount: Annotated[Money, Field(description="Tax amount for this rate")]
1732
+
1733
+
1734
+ class RefundItem(VatlyModel):
1735
+ """
1736
+ An item to refund from an order
1737
+ """
1738
+
1739
+ model_config = ConfigDict(
1740
+ extra="allow",
1741
+ populate_by_name=True,
1742
+ )
1743
+ item_id: Annotated[
1744
+ str,
1745
+ Field(
1746
+ alias="itemId",
1747
+ description="ID of the order line item to refund",
1748
+ examples=["order_item_Jk4pQrSvWm8NjLhYbUcP"],
1749
+ ),
1750
+ ]
1751
+ amount: Annotated[
1752
+ Money,
1753
+ Field(
1754
+ description="Amount to refund for this item (before taxes).\nCannot exceed the remaining refundable amount for the item.\n"
1755
+ ),
1756
+ ]
1757
+ description: Annotated[
1758
+ str | None,
1759
+ Field(
1760
+ description="Custom description for this refund line",
1761
+ examples=["Partial refund for service issue"],
1762
+ ),
1763
+ ] = None
1764
+ description_additional_line: Annotated[
1765
+ str | None,
1766
+ Field(
1767
+ alias="descriptionAdditionalLine",
1768
+ description="Additional description line for the refund",
1769
+ examples=["Ticket #12345"],
1770
+ ),
1771
+ ] = None
1772
+
1773
+
1774
+ class WebhookSubscriptionEventName(
1775
+ RootModel[
1776
+ Literal[
1777
+ "checkout.canceled",
1778
+ "checkout.expired",
1779
+ "checkout.failed",
1780
+ "checkout.paid",
1781
+ "one_off_product.archived",
1782
+ "one_off_product.unarchived",
1783
+ "one_off_product.update_approved",
1784
+ "one_off_product.update_rejected",
1785
+ "one_off_product.update_submitted",
1786
+ "order.canceled",
1787
+ "order.chargeback_received",
1788
+ "order.chargeback_reversed",
1789
+ "order.paid",
1790
+ "order.payment_failed",
1791
+ "refund.canceled",
1792
+ "refund.completed",
1793
+ "refund.failed",
1794
+ "subscription.billing_updated",
1795
+ "subscription.canceled_for_nonpayment",
1796
+ "subscription.canceled_immediately",
1797
+ "subscription.canceled_with_grace_period",
1798
+ "subscription.cancellation_grace_period_completed",
1799
+ "subscription.resumed",
1800
+ "subscription.started",
1801
+ "subscription.update_scheduled",
1802
+ "subscription.updated",
1803
+ "subscription_plan.archived",
1804
+ "subscription_plan.unarchived",
1805
+ "subscription_plan.update_approved",
1806
+ "subscription_plan.update_rejected",
1807
+ "subscription_plan.update_submitted",
1808
+ ]
1809
+ ]
1810
+ ):
1811
+ model_config = ConfigDict(
1812
+ populate_by_name=True,
1813
+ )
1814
+ root: Annotated[
1815
+ Literal[
1816
+ "checkout.canceled",
1817
+ "checkout.expired",
1818
+ "checkout.failed",
1819
+ "checkout.paid",
1820
+ "one_off_product.archived",
1821
+ "one_off_product.unarchived",
1822
+ "one_off_product.update_approved",
1823
+ "one_off_product.update_rejected",
1824
+ "one_off_product.update_submitted",
1825
+ "order.canceled",
1826
+ "order.chargeback_received",
1827
+ "order.chargeback_reversed",
1828
+ "order.paid",
1829
+ "order.payment_failed",
1830
+ "refund.canceled",
1831
+ "refund.completed",
1832
+ "refund.failed",
1833
+ "subscription.billing_updated",
1834
+ "subscription.canceled_for_nonpayment",
1835
+ "subscription.canceled_immediately",
1836
+ "subscription.canceled_with_grace_period",
1837
+ "subscription.cancellation_grace_period_completed",
1838
+ "subscription.resumed",
1839
+ "subscription.started",
1840
+ "subscription.update_scheduled",
1841
+ "subscription.updated",
1842
+ "subscription_plan.archived",
1843
+ "subscription_plan.unarchived",
1844
+ "subscription_plan.update_approved",
1845
+ "subscription_plan.update_rejected",
1846
+ "subscription_plan.update_submitted",
1847
+ ],
1848
+ Field(
1849
+ description="A public event name that can be enabled for a webhook endpoint. The\n`webhook.setup` verification event is deliberately excluded: Vatly sends\nthat event whenever it verifies endpoint configuration, regardless of the\nendpoint's subscriptions.\n",
1850
+ examples=["order.paid"],
1851
+ ),
1852
+ ]
1853
+
1854
+
1855
+ class FastForwardSubscriptionRenewalRequest(VatlyModel):
1856
+ model_config = ConfigDict(
1857
+ extra="allow",
1858
+ populate_by_name=True,
1859
+ )
1860
+ payment_status: Annotated[
1861
+ Literal["paid", "failed"] | None,
1862
+ Field(
1863
+ alias="paymentStatus",
1864
+ description="Deterministic outcome to simulate for the test-mode renewal payment. The\nhelper uses a local payment and does not submit a charge to the provider\nsandbox, so a provider response cannot override this requested status.\n\nOmit it to advance the billing cycle and leave that local payment\npending, which is this endpoint's original behaviour.\n\n`failed` declines the payment and starts a payment recovery for the\nrenewal order, so `order.payment_failed` is delivered to your webhook\nendpoint. `paid` settles it.\n",
1865
+ examples=["failed"],
1866
+ ),
1867
+ ] = None
1868
+ failure_reason: Annotated[
1869
+ Literal[
1870
+ "insufficient_funds",
1871
+ "temporary_decline",
1872
+ "general_failure",
1873
+ "invalid_mandate",
1874
+ "mandate_canceled",
1875
+ "account_closed",
1876
+ "card_expired",
1877
+ "card_lost_or_stolen",
1878
+ "invalid_card_details",
1879
+ "authentication_failed",
1880
+ "fraud_suspected",
1881
+ ]
1882
+ | None,
1883
+ Field(
1884
+ alias="failureReason",
1885
+ description="Which decline to simulate. Only valid alongside `paymentStatus: failed`;\nsending it with `paid` is rejected.\n\nThe reason decides how the payment recovery proceeds. A soft decline\n(`insufficient_funds`, `temporary_decline`, `general_failure`) means the\npayment method is still good, so recovery retries on a multi-week\ntimeline. Every other value is a hard decline: the payment method is\ndead, retrying is pointless, and recovery instead drives the customer to\nsupply a new one on a short timeline.\n\nDefaults to `general_failure`, which is soft.\n",
1886
+ examples=["card_expired"],
1887
+ ),
1888
+ ] = "general_failure"
1889
+
1890
+
1891
+ class SimulateOrderPaymentRequest(VatlyModel):
1892
+ model_config = ConfigDict(
1893
+ extra="allow",
1894
+ populate_by_name=True,
1895
+ )
1896
+ payment_status: Annotated[
1897
+ Literal["paid", "failed"],
1898
+ Field(
1899
+ alias="paymentStatus",
1900
+ description="Outcome to force on the order's pending payment.\n\n`paid` settles it, so `order.paid` is delivered to your webhook endpoint.\n`failed` declines it and starts a payment recovery, so\n`order.payment_failed` is delivered.\n",
1901
+ examples=["paid"],
1902
+ ),
1903
+ ]
1904
+ failure_reason: Annotated[
1905
+ Literal[
1906
+ "insufficient_funds",
1907
+ "temporary_decline",
1908
+ "general_failure",
1909
+ "invalid_mandate",
1910
+ "mandate_canceled",
1911
+ "account_closed",
1912
+ "card_expired",
1913
+ "card_lost_or_stolen",
1914
+ "invalid_card_details",
1915
+ "authentication_failed",
1916
+ "fraud_suspected",
1917
+ ]
1918
+ | None,
1919
+ Field(
1920
+ alias="failureReason",
1921
+ description="Which decline to simulate. Only valid alongside `paymentStatus: failed`;\nsending it with `paid` is rejected.\n\nThe reason decides how the payment recovery proceeds. A soft decline\n(`insufficient_funds`, `temporary_decline`, `general_failure`) means the\npayment method is still good, so recovery retries on a multi-week\ntimeline. Every other value is a hard decline: the payment method is\ndead, retrying is pointless, and recovery instead drives the customer to\nsupply a new one on a short timeline.\n\nDefaults to `general_failure`, which is soft.\n",
1922
+ examples=["card_expired"],
1923
+ ),
1924
+ ] = "general_failure"
1925
+
1926
+
1927
+ class Webhook(VatlyModel):
1928
+ """
1929
+ A registered webhook endpoint configuration — the subject of a `webhook.setup`
1930
+ verification call. Never includes the signing secret.
1931
+
1932
+ """
1933
+
1934
+ model_config = ConfigDict(
1935
+ extra="allow",
1936
+ populate_by_name=True,
1937
+ )
1938
+ resource: Annotated[Literal["webhook"], Field(examples=["webhook"])]
1939
+ id: Annotated[
1940
+ str,
1941
+ Field(
1942
+ description="The webhook endpoint identifier.",
1943
+ examples=["webhook_QdEpFhdSrG4Y3DnfsdqsH"],
1944
+ ),
1945
+ ]
1946
+ url: Annotated[
1947
+ str,
1948
+ Field(
1949
+ description="The endpoint the verification call was sent to.",
1950
+ examples=["https://merchant.example/webhooks/vatly"],
1951
+ ),
1952
+ ]
1953
+ testmode: Annotated[bool, Field(examples=[True])]
1954
+
1955
+
1956
+ class Links10(VatlyModel):
1957
+ """
1958
+ HATEOAS links to related resources. Present on event deliveries; omitted on
1959
+ the `webhook.setup` verification call, which is not a persisted resource.
1960
+
1961
+ """
1962
+
1963
+ model_config = ConfigDict(
1964
+ extra="allow",
1965
+ populate_by_name=True,
1966
+ )
1967
+ self: Annotated[Link, Field(description="Link to this webhook event resource")]
1968
+
1969
+
1970
+ class ListCheckoutsParams(VatlyModel):
1971
+ model_config = ConfigDict(
1972
+ extra="allow",
1973
+ populate_by_name=True,
1974
+ )
1975
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
1976
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
1977
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
1978
+
1979
+
1980
+ class ListCustomersParams(VatlyModel):
1981
+ model_config = ConfigDict(
1982
+ extra="allow",
1983
+ populate_by_name=True,
1984
+ )
1985
+ email: str | None = None
1986
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
1987
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
1988
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
1989
+
1990
+
1991
+ class ListCustomerSubscriptionsParams(VatlyModel):
1992
+ model_config = ConfigDict(
1993
+ extra="allow",
1994
+ populate_by_name=True,
1995
+ )
1996
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
1997
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
1998
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
1999
+
2000
+
2001
+ class ListOneOffProductsParams(VatlyModel):
2002
+ model_config = ConfigDict(
2003
+ extra="allow",
2004
+ populate_by_name=True,
2005
+ )
2006
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2007
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2008
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2009
+ include_archived: Annotated[bool | None, Field(alias="includeArchived")] = False
2010
+
2011
+
2012
+ class ListSubscriptionPlansParams(VatlyModel):
2013
+ model_config = ConfigDict(
2014
+ extra="allow",
2015
+ populate_by_name=True,
2016
+ )
2017
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2018
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2019
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2020
+ include_archived: Annotated[bool | None, Field(alias="includeArchived")] = False
2021
+
2022
+
2023
+ class ListOrdersParams(VatlyModel):
2024
+ model_config = ConfigDict(
2025
+ extra="allow",
2026
+ populate_by_name=True,
2027
+ )
2028
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2029
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2030
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2031
+
2032
+
2033
+ class ListOrderRefundsParams(VatlyModel):
2034
+ model_config = ConfigDict(
2035
+ extra="allow",
2036
+ populate_by_name=True,
2037
+ )
2038
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2039
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2040
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2041
+
2042
+
2043
+ class ListOrderChargebacksParams(VatlyModel):
2044
+ model_config = ConfigDict(
2045
+ extra="allow",
2046
+ populate_by_name=True,
2047
+ )
2048
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2049
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2050
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2051
+
2052
+
2053
+ class ListRefundsParams(VatlyModel):
2054
+ model_config = ConfigDict(
2055
+ extra="allow",
2056
+ populate_by_name=True,
2057
+ )
2058
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2059
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2060
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2061
+
2062
+
2063
+ class ListChargebacksParams(VatlyModel):
2064
+ model_config = ConfigDict(
2065
+ extra="allow",
2066
+ populate_by_name=True,
2067
+ )
2068
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2069
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2070
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2071
+
2072
+
2073
+ class ListWebhookEndpointsParams(VatlyModel):
2074
+ model_config = ConfigDict(
2075
+ extra="allow",
2076
+ populate_by_name=True,
2077
+ )
2078
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2079
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2080
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2081
+
2082
+
2083
+ class ListSubscriptionsParams(VatlyModel):
2084
+ model_config = ConfigDict(
2085
+ extra="allow",
2086
+ populate_by_name=True,
2087
+ )
2088
+ limit: Annotated[int | None, Field(ge=1, le=100)] = 10
2089
+ starting_after: Annotated[str | None, Field(alias="startingAfter")] = None
2090
+ ending_before: Annotated[str | None, Field(alias="endingBefore")] = None
2091
+
2092
+
2093
+ class CancelSubscriptionParams(VatlyModel):
2094
+ model_config = ConfigDict(
2095
+ extra="allow",
2096
+ populate_by_name=True,
2097
+ )
2098
+ immediately: bool | None = False
2099
+
2100
+
2101
+ class CreateSubscriptionBillingUpdateLinkResponse(VatlyModel):
2102
+ model_config = ConfigDict(
2103
+ extra="allow",
2104
+ populate_by_name=True,
2105
+ )
2106
+ href: Annotated[
2107
+ str,
2108
+ Field(
2109
+ description="URL to redirect customer for billing update",
2110
+ examples=[
2111
+ "https://vatly.com/subscriptions/subscription_Lp3mNvBxKw7RjTgYcZaE/billing?token=xyz..."
2112
+ ],
2113
+ ),
2114
+ ]
2115
+ type: Annotated[Literal["text/html"], Field(examples=["text/html"])]
2116
+
2117
+
2118
+ class OrderLine(VatlyModel):
2119
+ """
2120
+ A line item in an order
2121
+ """
2122
+
2123
+ model_config = ConfigDict(
2124
+ extra="allow",
2125
+ populate_by_name=True,
2126
+ )
2127
+ id: Annotated[
2128
+ str,
2129
+ Field(
2130
+ description="Unique identifier for this line item",
2131
+ examples=["order_item_Jk4pQrSvWm8NjLhYbUcP"],
2132
+ ),
2133
+ ]
2134
+ resource: Annotated[
2135
+ Literal["orderline"],
2136
+ Field(description="Resource type identifier", examples=["orderline"]),
2137
+ ]
2138
+ description: Annotated[
2139
+ str,
2140
+ Field(description="Description of the item", examples=["Pro Monthly Subscription"]),
2141
+ ]
2142
+ quantity: Annotated[int, Field(description="Number of units", examples=[1], ge=1)]
2143
+ base_price: Annotated[
2144
+ Money, Field(alias="basePrice", description="Price per unit before taxes")
2145
+ ]
2146
+ total: Annotated[
2147
+ Money,
2148
+ Field(description="Total price including taxes (basePrice * quantity + taxes)"),
2149
+ ]
2150
+ subtotal: Annotated[Money, Field(description="Subtotal before taxes (basePrice * quantity)")]
2151
+ taxes: Annotated[
2152
+ Sequence[TaxSummaryItem],
2153
+ Field(description="Tax breakdown by rate for this line"),
2154
+ ]
2155
+ product_type: Annotated[
2156
+ Literal[
2157
+ "one_off_product",
2158
+ "subscription_plan",
2159
+ "subscription",
2160
+ "billing_details_verification",
2161
+ "auth_request_product",
2162
+ ]
2163
+ | None,
2164
+ Field(
2165
+ alias="productType",
2166
+ description="The product type this line links to; null when the line has no product link.",
2167
+ examples=["subscription"],
2168
+ ),
2169
+ ] = None
2170
+ product_id: Annotated[
2171
+ str | None,
2172
+ Field(
2173
+ alias="productId",
2174
+ description="The linked product's id; for a `subscription` line this is the public `subscription_…` id.",
2175
+ examples=["subscription_Hn5xWqVfKm8RjTgYbUcP"],
2176
+ ),
2177
+ ] = None
2178
+
2179
+
2180
+ class RefundLine(VatlyModel):
2181
+ """
2182
+ A line item in a refund
2183
+ """
2184
+
2185
+ model_config = ConfigDict(
2186
+ extra="allow",
2187
+ populate_by_name=True,
2188
+ )
2189
+ id: Annotated[
2190
+ str,
2191
+ Field(
2192
+ description="Unique identifier for this refund line",
2193
+ examples=["refund_item_Tk7mNvBxKw2RjTgYcZaE"],
2194
+ ),
2195
+ ]
2196
+ resource: Annotated[
2197
+ Literal["refundline"],
2198
+ Field(description="Resource type identifier", examples=["refundline"]),
2199
+ ]
2200
+ description: Annotated[
2201
+ str,
2202
+ Field(
2203
+ description="Description of the refunded item",
2204
+ examples=["Pro Monthly Subscription (Refund)"],
2205
+ ),
2206
+ ]
2207
+ quantity: Annotated[
2208
+ int, Field(description="Number of units being refunded", examples=[1], ge=1)
2209
+ ]
2210
+ base_price: Annotated[
2211
+ Money,
2212
+ Field(alias="basePrice", description="Refund amount per unit before taxes"),
2213
+ ]
2214
+ total: Annotated[Money, Field(description="Total refund amount including taxes")]
2215
+ subtotal: Annotated[Money, Field(description="Refund subtotal before taxes")]
2216
+ taxes: Annotated[
2217
+ Sequence[TaxSummaryItem],
2218
+ Field(description="Tax breakdown by rate being refunded"),
2219
+ ]
2220
+
2221
+
2222
+ class CreateRefundRequest(VatlyModel):
2223
+ """
2224
+ Request body for creating a partial refund
2225
+ """
2226
+
2227
+ model_config = ConfigDict(
2228
+ extra="allow",
2229
+ populate_by_name=True,
2230
+ )
2231
+ items: Annotated[
2232
+ Sequence[RefundItem],
2233
+ Field(
2234
+ description="Items to refund",
2235
+ examples=[
2236
+ [
2237
+ {
2238
+ "itemId": "order_item_Jk4pQrSvWm8NjLhYbUcP",
2239
+ "amount": {"value": "15.00", "currency": "EUR"},
2240
+ "description": "Partial refund",
2241
+ }
2242
+ ]
2243
+ ],
2244
+ min_length=1,
2245
+ ),
2246
+ ]
2247
+ metadata: Metadata | None = None
2248
+
2249
+
2250
+ class Chargeback(VatlyModel):
2251
+ """
2252
+ A chargeback represents a payment dispute initiated by a customer through their bank or card issuer.
2253
+ Chargebacks are created automatically when the payment provider notifies Vatly of a dispute.
2254
+
2255
+ """
2256
+
2257
+ model_config = ConfigDict(
2258
+ extra="allow",
2259
+ populate_by_name=True,
2260
+ )
2261
+ id: Annotated[
2262
+ str,
2263
+ Field(
2264
+ description="Unique identifier for the chargeback",
2265
+ examples=["chargeback_Mn6xBtPvKw2RjTgYcZaE"],
2266
+ ),
2267
+ ]
2268
+ resource: Annotated[
2269
+ Literal["chargeback"],
2270
+ Field(description="Resource type identifier", examples=["chargeback"]),
2271
+ ]
2272
+ customer_id: Annotated[
2273
+ str,
2274
+ Field(
2275
+ alias="customerId",
2276
+ description="ID of the customer the chargeback was raised against",
2277
+ examples=["customer_Lp3mNvBxKw7RjTgYcZaE"],
2278
+ ),
2279
+ ]
2280
+ testmode: Annotated[
2281
+ bool,
2282
+ Field(description="Whether this chargeback is in test mode", examples=[False]),
2283
+ ]
2284
+ created_at: Annotated[
2285
+ AwareDatetime,
2286
+ Field(
2287
+ alias="createdAt",
2288
+ description="When this chargeback was created (ISO 8601 format)",
2289
+ examples=["2024-01-15T10:30:00Z"],
2290
+ ),
2291
+ ]
2292
+ status: Annotated[
2293
+ Literal[
2294
+ "pending",
2295
+ "requires_amount_review",
2296
+ "accepted",
2297
+ "rejected",
2298
+ "evidence_submitted",
2299
+ "won",
2300
+ "lost",
2301
+ ],
2302
+ Field(
2303
+ description="Current status of the chargeback dispute:\n- `pending` - Dispute received, awaiting action\n- `requires_amount_review` - Provider amount exceeds the original order; financial completion is quarantined pending review\n- `accepted` - Merchant accepted the dispute\n- `rejected` - Dispute rejected\n- `evidence_submitted` - Evidence submitted, awaiting decision\n- `won` - Dispute resolved in the merchant's favor\n- `lost` - Dispute resolved in the customer's favor\n",
2304
+ examples=["pending"],
2305
+ ),
2306
+ ]
2307
+ amount: Annotated[Money, Field(description="Amount of the chargeback")]
2308
+ settlement_amount: Annotated[
2309
+ Money,
2310
+ Field(
2311
+ alias="settlementAmount",
2312
+ description="Amount that was deducted from the merchant's settlement.\nMay differ from `amount` due to currency conversion or fees.\n",
2313
+ ),
2314
+ ]
2315
+ total: Annotated[Money, Field(description="Total chargeback amount including taxes")]
2316
+ subtotal: Annotated[Money, Field(description="Chargeback subtotal before taxes")]
2317
+ tax_summary: Annotated[
2318
+ Sequence[TaxSummaryItem],
2319
+ Field(alias="taxSummary", description="Tax breakdown by rate for the chargeback"),
2320
+ ]
2321
+ reason: Annotated[
2322
+ str,
2323
+ Field(
2324
+ description="Reason code or description for the chargeback.\nCommon reasons include:\n- `fraud` - Unauthorized transaction\n- `product_not_received` - Customer claims non-delivery\n- `product_unacceptable` - Customer claims product was defective\n- `duplicate` - Customer claims duplicate charge\n- `subscription_canceled` - Customer claims subscription was canceled\n",
2325
+ examples=["fraud"],
2326
+ ),
2327
+ ]
2328
+ original_order_id: Annotated[
2329
+ str,
2330
+ Field(
2331
+ alias="originalOrderId",
2332
+ description="ID of the original order that was charged back",
2333
+ examples=["order_Fp2kQrSvWm8NjLhYbUcP"],
2334
+ ),
2335
+ ]
2336
+ order_id: Annotated[
2337
+ str | None,
2338
+ Field(
2339
+ alias="orderId",
2340
+ description="ID of the credit note order created for this chargeback. Always present\nin the payload; `null` until the chargeback is processed, then set to the\ncredit-note order id.\n",
2341
+ examples=["order_Rk5pQrSvWm8NjLhYbUcP"],
2342
+ ),
2343
+ ] = None
2344
+ links: Annotated[Links6, Field(description="HATEOAS links to related resources")]
2345
+
2346
+
2347
+ class CreateWebhookEndpointRequest(VatlyModel):
2348
+ """
2349
+ Request body for registering a new webhook endpoint
2350
+ """
2351
+
2352
+ model_config = ConfigDict(
2353
+ extra="allow",
2354
+ populate_by_name=True,
2355
+ )
2356
+ url: Annotated[
2357
+ str,
2358
+ Field(
2359
+ description="The HTTPS URL deliveries are POSTed to. Must be publicly reachable and\npresent a valid SSL certificate — Vatly sends a `webhook.setup`\nverification ping on registration and rejects the request if it fails.\n`localhost` and loopback addresses are not allowed.\n",
2360
+ examples=["https://merchant.example/webhooks/vatly"],
2361
+ ),
2362
+ ]
2363
+ secret: Annotated[
2364
+ str,
2365
+ Field(
2366
+ description="Signing secret for this endpoint, used to compute the `Vatly-Signature`\nHMAC on every delivery. You choose it (e.g. pinned from an environment\nvariable) and use it to verify inbound webhooks. It is write-only — keep\nthis value, as the API never returns it.\n",
2367
+ examples=["whsec_3f9a1c7e2d4f7b9c5a2c1d5b7e9f3a8d"],
2368
+ min_length=10,
2369
+ ),
2370
+ ]
2371
+ enabled_events: Annotated[
2372
+ Sequence[WebhookSubscriptionEventName] | None,
2373
+ Field(
2374
+ alias="enabledEvents",
2375
+ description="The explicit events delivered to this endpoint. If omitted, Vatly\nstores all event names available at registration time. New event names\nadded later are not enabled automatically. Send an empty list to create\na dormant endpoint. `webhook.setup` cannot be selected and remains an\nunconditional configuration-verification event.\n",
2376
+ examples=[["order.paid", "refund.completed"]],
2377
+ ),
2378
+ ] = None
2379
+
2380
+
2381
+ class UpdateWebhookEndpointRequest(VatlyModel):
2382
+ """
2383
+ Request body for updating a webhook endpoint. Send `url` to repoint the
2384
+ endpoint, `secret` to rotate the signing secret, and/or `enabledEvents` to
2385
+ replace its full subscription set. Omitting all three is a no-op that
2386
+ returns the current endpoint.
2387
+
2388
+ """
2389
+
2390
+ model_config = ConfigDict(
2391
+ extra="allow",
2392
+ populate_by_name=True,
2393
+ )
2394
+ url: Annotated[
2395
+ str | None,
2396
+ Field(
2397
+ description="New HTTPS delivery URL. Revalidated for reachability and SSL the same\nway as on creation.\n",
2398
+ examples=["https://merchant.example/webhooks/vatly-v2"],
2399
+ ),
2400
+ ] = None
2401
+ secret: Annotated[
2402
+ str | None,
2403
+ Field(
2404
+ description="New signing secret for this endpoint. Write-only — keep the value, as\nthe API never returns it.\n",
2405
+ examples=["whsec_a1b2c3d4e5f60718293a4b5c6d7e8f90"],
2406
+ min_length=10,
2407
+ ),
2408
+ ] = None
2409
+ enabled_events: Annotated[
2410
+ Sequence[WebhookSubscriptionEventName] | None,
2411
+ Field(
2412
+ alias="enabledEvents",
2413
+ description="Replaces the endpoint's complete event subscription set. Omit this\nfield to preserve the current subscriptions; send an empty list to make\nthe endpoint dormant. New event names are never added automatically.\n`webhook.setup` is not subscribable.\n",
2414
+ examples=[["checkout.paid", "order.paid"]],
2415
+ ),
2416
+ ] = None
2417
+
2418
+
2419
+ class Subscription(VatlyModel):
2420
+ """
2421
+ A subscription represents a recurring billing relationship with a customer.
2422
+ Subscriptions automatically renew according to their billing interval.
2423
+
2424
+ """
2425
+
2426
+ model_config = ConfigDict(
2427
+ extra="allow",
2428
+ populate_by_name=True,
2429
+ )
2430
+ id: Annotated[
2431
+ str,
2432
+ Field(
2433
+ description="Unique identifier for the subscription",
2434
+ examples=["subscription_Lp3mNvBxKw7RjTgYcZaE"],
2435
+ ),
2436
+ ]
2437
+ resource: Annotated[
2438
+ Literal["subscription"],
2439
+ Field(description="Resource type identifier", examples=["subscription"]),
2440
+ ]
2441
+ customer_id: Annotated[
2442
+ str,
2443
+ Field(
2444
+ alias="customerId",
2445
+ description="ID of the customer who owns this subscription",
2446
+ examples=["customer_Lp3mNvBxKw7RjTgYcZaE"],
2447
+ ),
2448
+ ]
2449
+ subscription_plan_id: Annotated[
2450
+ str,
2451
+ Field(
2452
+ alias="subscriptionPlanId",
2453
+ description="ID of the subscription plan this subscription is based on",
2454
+ examples=["subscription_plan_Wt5mNvBxKw7YcZaEjLhR"],
2455
+ ),
2456
+ ]
2457
+ testmode: Annotated[
2458
+ bool,
2459
+ Field(description="Whether this subscription is in test mode", examples=[False]),
2460
+ ]
2461
+ name: Annotated[
2462
+ str,
2463
+ Field(
2464
+ description="Name of the subscription (from the plan)",
2465
+ examples=["Pro Monthly"],
2466
+ ),
2467
+ ]
2468
+ description: Annotated[
2469
+ str,
2470
+ Field(
2471
+ description="Description of the subscription",
2472
+ examples=["Full access to all Pro features"],
2473
+ ),
2474
+ ]
2475
+ billing_address: Annotated[
2476
+ BillingAddress,
2477
+ Field(
2478
+ alias="billingAddress",
2479
+ description="Customer's billing address for this subscription",
2480
+ ),
2481
+ ]
2482
+ base_price: Annotated[
2483
+ Money,
2484
+ Field(alias="basePrice", description="Price per billing cycle before taxes"),
2485
+ ]
2486
+ quantity: Annotated[
2487
+ int,
2488
+ Field(description="Number of subscription units (e.g., seats)", examples=[1], ge=1),
2489
+ ]
2490
+ interval: Annotated[
2491
+ Literal["day", "week", "month", "year"],
2492
+ Field(description="Billing interval unit", examples=["month"]),
2493
+ ]
2494
+ interval_count: Annotated[
2495
+ int,
2496
+ Field(
2497
+ alias="intervalCount",
2498
+ description="Number of interval units between billing cycles",
2499
+ examples=[1],
2500
+ ge=1,
2501
+ ),
2502
+ ]
2503
+ status: Annotated[
2504
+ Literal["active", "created", "trial", "on_grace_period", "paused", "canceled"],
2505
+ Field(
2506
+ description="Current status of the subscription:\n- `active` - Subscription is active and will renew\n- `created` - Subscription has been created but not yet started\n- `trial` - Subscription is in trial period\n- `on_grace_period` - Subscription is canceled but still active until period ends\n- `paused` - Subscription is temporarily paused\n- `canceled` - Subscription has been canceled\n",
2507
+ examples=["active"],
2508
+ ),
2509
+ ]
2510
+ cancellation_reason: Annotated[
2511
+ Literal["payment_failure", "merchant_request", "customer_request"] | None,
2512
+ Field(
2513
+ alias="cancellationReason",
2514
+ description="Why the subscription was canceled. Null unless a cancellation has been\nrequested. Public values are intentionally decoupled from internal domain\nreason names:\n- `payment_failure` - Payment recovery was exhausted after failed renewals\n- `merchant_request` - The merchant explicitly canceled the subscription\n- `customer_request` - The customer canceled from the self-service portal\n",
2515
+ examples=[None],
2516
+ ),
2517
+ ]
2518
+ mandate: Annotated[
2519
+ Mandate | None,
2520
+ Field(
2521
+ description='Payment method on file for this subscription, as an inline summary\nintended for billing-portal rendering ("Visa ending in 4242"). May\nbe `null` when the subscription has no mandate yet (e.g. ended /\ncanceled-before-payment states).\n'
2522
+ ),
2523
+ ] = None
2524
+ started_at: Annotated[
2525
+ AwareDatetime | None,
2526
+ Field(
2527
+ alias="startedAt",
2528
+ description="When the subscription started (ISO 8601 format)",
2529
+ examples=["2024-01-15T10:30:00Z"],
2530
+ ),
2531
+ ] = None
2532
+ ended_at: Annotated[
2533
+ AwareDatetime | None,
2534
+ Field(alias="endedAt", description="When the subscription ended (ISO 8601 format)"),
2535
+ ] = None
2536
+ canceled_at: Annotated[
2537
+ AwareDatetime | None,
2538
+ Field(
2539
+ alias="canceledAt",
2540
+ description="When the subscription was canceled (ISO 8601 format)",
2541
+ ),
2542
+ ] = None
2543
+ renewed_at: Annotated[
2544
+ AwareDatetime | None,
2545
+ Field(
2546
+ alias="renewedAt",
2547
+ description="When the subscription was last renewed (ISO 8601 format)",
2548
+ examples=["2024-02-15T10:30:00Z"],
2549
+ ),
2550
+ ] = None
2551
+ renewed_until: Annotated[
2552
+ AwareDatetime | None,
2553
+ Field(
2554
+ alias="renewedUntil",
2555
+ description="Current billing period end date (ISO 8601 format)",
2556
+ examples=["2024-03-15T10:30:00Z"],
2557
+ ),
2558
+ ] = None
2559
+ next_renewal_at: Annotated[
2560
+ AwareDatetime | None,
2561
+ Field(
2562
+ alias="nextRenewalAt",
2563
+ description="When the next renewal will be attempted (ISO 8601 format).\nNull if subscription is canceled or ended.\n",
2564
+ examples=["2024-03-15T10:30:00Z"],
2565
+ ),
2566
+ ] = None
2567
+ trial_until: Annotated[
2568
+ AwareDatetime | None,
2569
+ Field(
2570
+ alias="trialUntil",
2571
+ description="When the trial period ends (ISO 8601 format).\nNull if not in trial or trial has ended.\n",
2572
+ ),
2573
+ ] = None
2574
+ links: Annotated[Links9, Field(description="HATEOAS links to related resources")]
2575
+ scheduled_update: Annotated[
2576
+ ScheduledUpdate | None,
2577
+ Field(
2578
+ alias="scheduledUpdate",
2579
+ description="The target values for a change scheduled to take effect at the subscription's next\nbilling cycle — set by `PATCH /subscriptions/{subscriptionId}` with\n`applyImmediately: false`. The rest of the object is the subscription's *current*\nstate; `scheduledUpdate` is what it will become.\n\nNull when nothing is pending. Always present on both the REST resource and webhook\ndeliveries, so this is the authoritative way to reconcile a pending change — do not\ndepend on having received the `subscription.update_scheduled` webhook, which is\ndelivered once.\n\nCleared when the change is applied at renewal, and when it is discarded because the\nsubscription was canceled before that renewal happened.\n",
2580
+ ),
2581
+ ] = None
2582
+
2583
+
2584
+ class Order(VatlyModel):
2585
+ """
2586
+ An order represents a completed purchase.
2587
+ Orders are created when checkouts complete or subscriptions renew.
2588
+
2589
+ """
2590
+
2591
+ model_config = ConfigDict(
2592
+ extra="allow",
2593
+ populate_by_name=True,
2594
+ )
2595
+ id: Annotated[
2596
+ str,
2597
+ Field(
2598
+ description="Unique identifier for the order",
2599
+ examples=["order_Hn5xWqVfKm8RjTgYbUcP"],
2600
+ ),
2601
+ ]
2602
+ resource: Annotated[
2603
+ Literal["order"],
2604
+ Field(description="Resource type identifier", examples=["order"]),
2605
+ ]
2606
+ customer_id: Annotated[
2607
+ str | None,
2608
+ Field(
2609
+ alias="customerId",
2610
+ description="ID of the customer who made this purchase. Always present; `null` for orders with no associated customer.",
2611
+ examples=["customer_Lp3mNvBxKw7RjTgYcZaE"],
2612
+ ),
2613
+ ] = None
2614
+ testmode: Annotated[
2615
+ bool, Field(description="Whether this order is in test mode", examples=[False])
2616
+ ]
2617
+ metadata: Metadata | None = None
2618
+ payment_method: Annotated[
2619
+ str | None,
2620
+ Field(
2621
+ alias="paymentMethod",
2622
+ description="Payment method used for this order.\nExamples: `ideal`, `creditcard`, `bancontact`, `paypal`\n",
2623
+ examples=["ideal"],
2624
+ ),
2625
+ ] = None
2626
+ created_at: Annotated[
2627
+ AwareDatetime,
2628
+ Field(
2629
+ alias="createdAt",
2630
+ description="When this order was created (ISO 8601 format)",
2631
+ examples=["2024-01-15T10:30:00Z"],
2632
+ ),
2633
+ ]
2634
+ status: Annotated[
2635
+ Literal["created", "pending", "paid", "canceled", "expired"],
2636
+ Field(
2637
+ description="Current status of the order:\n- `created` - Order has just been created\n- `pending` - Payment is being processed\n- `paid` - Payment successful\n- `canceled` - Order has been canceled\n- `expired` - Order has expired\n",
2638
+ examples=["paid"],
2639
+ ),
2640
+ ]
2641
+ cancellation_reason: Annotated[
2642
+ Literal["payment_failure"] | None,
2643
+ Field(
2644
+ alias="cancellationReason",
2645
+ description="Why the order was canceled. Null unless the status is `canceled`, and null\nfor orders canceled before this field existed.\n- `payment_failure` - Payment recovery for this unpaid order was exhausted\n",
2646
+ examples=[None],
2647
+ ),
2648
+ ]
2649
+ invoice_number: Annotated[
2650
+ str | None,
2651
+ Field(
2652
+ alias="invoiceNumber",
2653
+ description="Invoice number for this order (assigned after payment)",
2654
+ examples=["INV-2024-0001"],
2655
+ ),
2656
+ ] = None
2657
+ total: Annotated[Money, Field(description="Total amount including taxes")]
2658
+ subtotal: Annotated[Money, Field(description="Subtotal amount before taxes")]
2659
+ reversed_subtotal: Annotated[
2660
+ Money,
2661
+ Field(
2662
+ alias="reversedSubtotal",
2663
+ description="Amount of the captured payment returned to the customer, before taxes, via refund or chargeback (settled)",
2664
+ ),
2665
+ ]
2666
+ refundable_subtotal: Annotated[
2667
+ Money,
2668
+ Field(
2669
+ alias="refundableSubtotal",
2670
+ description="Remaining amount that can still be refunded, before taxes.",
2671
+ ),
2672
+ ]
2673
+ tax_summary: Annotated[
2674
+ Sequence[TaxSummaryItem],
2675
+ Field(alias="taxSummary", description="Tax breakdown by rate"),
2676
+ ]
2677
+ lines: Annotated[Sequence[OrderLine], Field(description="Line items in this order")]
2678
+ customer_details: Annotated[
2679
+ BillingDetails,
2680
+ Field(alias="customerDetails", description="Customer billing details (buyer)"),
2681
+ ]
2682
+ links: Annotated[Links4, Field(description="HATEOAS links to related resources")]
2683
+
2684
+
2685
+ class Refund(VatlyModel):
2686
+ """
2687
+ A refund represents a partial or full reversal of an order payment.
2688
+ Refunds are processed through the original payment provider.
2689
+
2690
+ """
2691
+
2692
+ model_config = ConfigDict(
2693
+ extra="allow",
2694
+ populate_by_name=True,
2695
+ )
2696
+ id: Annotated[
2697
+ str,
2698
+ Field(
2699
+ description="Unique identifier for the refund",
2700
+ examples=["refund_Xk9pQrSvWm4NjLhYbUcP"],
2701
+ ),
2702
+ ]
2703
+ resource: Annotated[
2704
+ Literal["refund"],
2705
+ Field(description="Resource type identifier", examples=["refund"]),
2706
+ ]
2707
+ order_id: Annotated[
2708
+ str | None,
2709
+ Field(
2710
+ alias="orderId",
2711
+ description="ID of the credit note order created for this refund. Always present in\nthe payload; `null` until the refund is processed, then set to the\ncredit-note order id.\n",
2712
+ examples=["order_Vr8kQdFhSrG4Y3DnfsdqH"],
2713
+ ),
2714
+ ] = None
2715
+ customer_id: Annotated[
2716
+ str,
2717
+ Field(
2718
+ alias="customerId",
2719
+ description="ID of the customer receiving the refund",
2720
+ examples=["customer_Lp3mNvBxKw7RjTgYcZaE"],
2721
+ ),
2722
+ ]
2723
+ testmode: Annotated[
2724
+ bool, Field(description="Whether this refund is in test mode", examples=[False])
2725
+ ]
2726
+ created_at: Annotated[
2727
+ AwareDatetime,
2728
+ Field(
2729
+ alias="createdAt",
2730
+ description="When this refund was created (ISO 8601 format)",
2731
+ examples=["2024-01-15T10:30:00Z"],
2732
+ ),
2733
+ ]
2734
+ status: Annotated[
2735
+ Literal["pending", "queued", "processing", "refunded", "failed", "canceled"],
2736
+ Field(
2737
+ description="Current status of the refund:\n- `pending` - Refund is ready to be sent to the bank\n- `queued` - Refund is queued due to a lack of balance\n- `processing` - Refund is being processed (cancellation no longer possible)\n- `refunded` - Refund has been processed successfully\n- `failed` - Refund has failed after processing\n- `canceled` - Refund was canceled\n",
2738
+ examples=["refunded"],
2739
+ ),
2740
+ ]
2741
+ original_order_id: Annotated[
2742
+ str,
2743
+ Field(
2744
+ alias="originalOrderId",
2745
+ description="ID of the original order being refunded",
2746
+ examples=["order_Fp2kQrSvWm8NjLhYbUcP"],
2747
+ ),
2748
+ ]
2749
+ total: Annotated[Money, Field(description="Total refund amount including taxes")]
2750
+ subtotal: Annotated[Money, Field(description="Refund subtotal before taxes")]
2751
+ tax_summary: Annotated[
2752
+ Sequence[TaxSummaryItem],
2753
+ Field(alias="taxSummary", description="Tax breakdown by rate being refunded"),
2754
+ ]
2755
+ lines: Annotated[Sequence[RefundLine], Field(description="Refund line items")]
2756
+ links: Annotated[Links5, Field(description="HATEOAS links to related resources")]
2757
+
2758
+
2759
+ class WebhookEvent(VatlyModel):
2760
+ """
2761
+ A persisted webhook event — the record returned by `GET /webhook-events/{eventId}`.
2762
+ It captures a domain event that was delivered to your endpoint, including the full
2763
+ resource payload at the time the event occurred.
2764
+
2765
+ This is the *stored* resource. For the shape Vatly POSTs to your endpoint on each
2766
+ live delivery (which additionally includes the `webhook.setup` verification call),
2767
+ see `WebhookDelivery`.
2768
+
2769
+ """
2770
+
2771
+ model_config = ConfigDict(
2772
+ extra="allow",
2773
+ populate_by_name=True,
2774
+ )
2775
+ id: Annotated[
2776
+ str,
2777
+ Field(
2778
+ description="Unique identifier for the webhook event",
2779
+ examples=["webhook_event_Qk8pRtSvWm2NjLhYcZaE"],
2780
+ ),
2781
+ ]
2782
+ resource: Annotated[
2783
+ Literal["webhook_event"],
2784
+ Field(description="Resource type identifier", examples=["webhook_event"]),
2785
+ ]
2786
+ event_name: Annotated[
2787
+ Literal[
2788
+ "order.paid",
2789
+ "order.canceled",
2790
+ "order.chargeback_received",
2791
+ "order.chargeback_reversed",
2792
+ "order.payment_failed",
2793
+ "refund.completed",
2794
+ "refund.failed",
2795
+ "refund.canceled",
2796
+ "subscription.started",
2797
+ "subscription.canceled_immediately",
2798
+ "subscription.canceled_for_nonpayment",
2799
+ "subscription.canceled_with_grace_period",
2800
+ "subscription.cancellation_grace_period_completed",
2801
+ "subscription.resumed",
2802
+ "subscription.billing_updated",
2803
+ "subscription.updated",
2804
+ "subscription.update_scheduled",
2805
+ "checkout.paid",
2806
+ "checkout.failed",
2807
+ "checkout.canceled",
2808
+ "checkout.expired",
2809
+ "one_off_product.update_submitted",
2810
+ "one_off_product.update_approved",
2811
+ "one_off_product.update_rejected",
2812
+ "one_off_product.archived",
2813
+ "one_off_product.unarchived",
2814
+ "subscription_plan.update_submitted",
2815
+ "subscription_plan.update_approved",
2816
+ "subscription_plan.update_rejected",
2817
+ "subscription_plan.archived",
2818
+ "subscription_plan.unarchived",
2819
+ ],
2820
+ Field(
2821
+ alias="eventName",
2822
+ description="Name of the event that triggered this webhook:\n- `order.paid` - Order payment was successful\n- `order.canceled` - Order was canceled\n- `order.chargeback_received` - Chargeback was received for an order\n- `order.chargeback_reversed` - Chargeback was reversed\n- `order.payment_failed` - A payment failed and the order entered payment recovery (dunning); sent once per failed order\n- `refund.completed` - Refund was processed successfully\n- `refund.failed` - Refund processing failed\n- `refund.canceled` - Refund was canceled\n- `subscription.started` - Subscription was started\n- `subscription.canceled_immediately` - Subscription was canceled immediately\n- `subscription.canceled_for_nonpayment` - Subscription was canceled because payment recovery was exhausted\n- `subscription.canceled_with_grace_period` - Subscription was canceled with a grace period\n- `subscription.cancellation_grace_period_completed` - Subscription grace period ended\n- `subscription.resumed` - A canceled subscription was resumed during its grace period\n- `subscription.billing_updated` - A subscription's billing details changed; in practice today the mandate (payment method on file), e.g. to refresh the masked payment method identifier shown to the customer\n- `subscription.updated` - A subscription changed: on an immediate change, when a scheduled change takes effect at renewal, or when a renewal payment is confirmed; see `reason`\n- `subscription.update_scheduled` - A plan/price/quantity change was scheduled for the next billing cycle; the target values are in `object.scheduledUpdate`\n- `checkout.paid` - Checkout was paid successfully\n- `checkout.failed` - Checkout payment failed\n- `checkout.canceled` - Checkout was canceled\n- `checkout.expired` - Checkout session expired\n\n`webhook.setup` is intentionally absent: the verification handshake is not\npersisted and so never appears on this resource (see `WebhookDelivery`).\n\nThis list is kept in sync with the webhook transformer registry by\n`php artisan openapi:audit-webhook-drift`.\n",
2823
+ examples=["order.paid"],
2824
+ ),
2825
+ ]
2826
+ reason: Annotated[
2827
+ str | None,
2828
+ Field(
2829
+ description="Why the event fired, for events that have more than one cause. Always\npresent; `null` for every event other than `subscription.updated`.\n\nFor `subscription.updated`:\n- `updated_immediately` - A plan, price, interval or quantity change took effect immediately\n- `updated_on_renewal` - A scheduled plan, price, interval or quantity change took effect at a renewal\n- `renewed` - A renewal payment was confirmed; `renewedAt` and `renewedUntil` moved\n\nNew values may be added; treat an unknown value as a generic change.\n",
2830
+ examples=["renewed"],
2831
+ ),
2832
+ ] = None
2833
+ entity_type: Annotated[
2834
+ str,
2835
+ Field(
2836
+ alias="entityType",
2837
+ description="Type of the resource this event relates to",
2838
+ examples=["order"],
2839
+ ),
2840
+ ]
2841
+ entity_id: Annotated[
2842
+ str,
2843
+ Field(
2844
+ alias="entityId",
2845
+ description="ID of the resource this event relates to",
2846
+ examples=["order_Hn5xWqVfKm8RjTgYbUcP"],
2847
+ ),
2848
+ ]
2849
+ object: Annotated[
2850
+ Order | Chargeback | Refund | Subscription | Checkout | OneOffProduct | SubscriptionPlan,
2851
+ Field(
2852
+ description="The full resource payload at the time of the event, serialized identically\nto the corresponding REST resource. The concrete shape is selected by the\npayload's own `resource` field (the discriminator):\n - `order.paid`, `order.canceled`, `order.payment_failed` → Order\n - `order.chargeback_received`, `order.chargeback_reversed` → Chargeback\n - `refund.*` → Refund\n - `subscription.*` → Subscription\n - `checkout.*` → Checkout\n - `one_off_product.*` → OneOffProduct\n - `subscription_plan.*` → SubscriptionPlan\n\nNote `object.resource` may differ from `entityType`: chargeback events carry\n`entityType: order` (the order the chargeback belongs to) but a Chargeback object.\n",
2853
+ discriminator="resource",
2854
+ ),
2855
+ ]
2856
+ created_at: Annotated[
2857
+ AwareDatetime,
2858
+ Field(
2859
+ alias="createdAt",
2860
+ description="ISO 8601 timestamp of when the webhook event was created.",
2861
+ examples=["2026-05-21T14:32:08+00:00"],
2862
+ ),
2863
+ ]
2864
+ testmode: Annotated[
2865
+ bool,
2866
+ Field(
2867
+ description="Whether the event originated from a test-mode resource.\n`true` for events created with a `test_` prefixed token, `false` for live events.\n",
2868
+ examples=[False],
2869
+ ),
2870
+ ]
2871
+ links: Annotated[
2872
+ Links7,
2873
+ Field(
2874
+ description="HATEOAS links to related resources. Always present on the persisted event."
2875
+ ),
2876
+ ]
2877
+
2878
+
2879
+ class WebhookDelivery(VatlyModel):
2880
+ """
2881
+ The envelope Vatly POSTs to your configured webhook endpoint for each delivery.
2882
+
2883
+ Identical to the persisted `WebhookEvent`, with two differences that exist only on
2884
+ the wire:
2885
+ - it additionally covers the `webhook.setup` verification call (`entityType`
2886
+ `webhook`, `object` is the secret-free endpoint config), and
2887
+ - that setup call is not persisted and omits `links`, so `links` is not required
2888
+ on a delivery.
2889
+
2890
+ Every delivery is signed: the `Vatly-Signature` header carries an HMAC-SHA256 of
2891
+ the raw body and `Vatly-Event-Id` carries a retry-stable dedup key (see the
2892
+ operation parameters).
2893
+
2894
+ """
2895
+
2896
+ model_config = ConfigDict(
2897
+ extra="allow",
2898
+ populate_by_name=True,
2899
+ )
2900
+ id: Annotated[
2901
+ str,
2902
+ Field(
2903
+ description="Unique identifier for the webhook event",
2904
+ examples=["webhook_event_Qk8pRtSvWm2NjLhYcZaE"],
2905
+ ),
2906
+ ]
2907
+ resource: Annotated[
2908
+ Literal["webhook_event"],
2909
+ Field(description="Resource type identifier", examples=["webhook_event"]),
2910
+ ]
2911
+ event_name: Annotated[
2912
+ Literal[
2913
+ "order.paid",
2914
+ "order.canceled",
2915
+ "order.chargeback_received",
2916
+ "order.chargeback_reversed",
2917
+ "order.payment_failed",
2918
+ "refund.completed",
2919
+ "refund.failed",
2920
+ "refund.canceled",
2921
+ "subscription.started",
2922
+ "subscription.canceled_immediately",
2923
+ "subscription.canceled_for_nonpayment",
2924
+ "subscription.canceled_with_grace_period",
2925
+ "subscription.cancellation_grace_period_completed",
2926
+ "subscription.resumed",
2927
+ "subscription.billing_updated",
2928
+ "subscription.updated",
2929
+ "subscription.update_scheduled",
2930
+ "checkout.paid",
2931
+ "checkout.failed",
2932
+ "checkout.canceled",
2933
+ "checkout.expired",
2934
+ "one_off_product.update_submitted",
2935
+ "one_off_product.update_approved",
2936
+ "one_off_product.update_rejected",
2937
+ "one_off_product.archived",
2938
+ "one_off_product.unarchived",
2939
+ "subscription_plan.update_submitted",
2940
+ "subscription_plan.update_approved",
2941
+ "subscription_plan.update_rejected",
2942
+ "subscription_plan.archived",
2943
+ "subscription_plan.unarchived",
2944
+ "webhook.setup",
2945
+ ],
2946
+ Field(
2947
+ alias="eventName",
2948
+ description="Name of the event that triggered this webhook:\n- `order.paid` - Order payment was successful\n- `order.canceled` - Order was canceled\n- `order.chargeback_received` - Chargeback was received for an order\n- `order.chargeback_reversed` - Chargeback was reversed\n- `order.payment_failed` - A payment failed and the order entered payment recovery (dunning); sent once per failed order\n- `refund.completed` - Refund was processed successfully\n- `refund.failed` - Refund processing failed\n- `refund.canceled` - Refund was canceled\n- `subscription.started` - Subscription was started\n- `subscription.canceled_immediately` - Subscription was canceled immediately\n- `subscription.canceled_for_nonpayment` - Subscription was canceled because payment recovery was exhausted\n- `subscription.canceled_with_grace_period` - Subscription was canceled with a grace period\n- `subscription.cancellation_grace_period_completed` - Subscription grace period ended\n- `subscription.resumed` - A canceled subscription was resumed during its grace period\n- `subscription.billing_updated` - A subscription's billing details changed; in practice today the mandate (payment method on file), e.g. to refresh the masked payment method identifier shown to the customer\n- `subscription.updated` - A subscription changed: on an immediate change, when a scheduled change takes effect at renewal, or when a renewal payment is confirmed; see `reason`\n- `subscription.update_scheduled` - A plan/price/quantity change was scheduled for the next billing cycle; the target values are in `object.scheduledUpdate`\n- `checkout.paid` - Checkout was paid successfully\n- `checkout.failed` - Checkout payment failed\n- `checkout.canceled` - Checkout was canceled\n- `checkout.expired` - Checkout session expired\n- `webhook.setup` - Verification call sent when an endpoint is registered or its URL changes (`entityType` is `webhook`)\n\nThis list is the single source of truth for SDK generators and is kept in\nsync with the webhook transformer registry by `php artisan openapi:audit-webhook-drift`.\n",
2949
+ examples=["order.paid"],
2950
+ ),
2951
+ ]
2952
+ reason: Annotated[
2953
+ str | None,
2954
+ Field(
2955
+ description="Why the event fired, for events that have more than one cause. Always\npresent; `null` for every event other than `subscription.updated`.\n\nFor `subscription.updated`:\n- `updated_immediately` - A plan, price, interval or quantity change took effect immediately\n- `updated_on_renewal` - A scheduled plan, price, interval or quantity change took effect at a renewal\n- `renewed` - A renewal payment was confirmed; `renewedAt` and `renewedUntil` moved\n\nNew values may be added; treat an unknown value as a generic change.\n",
2956
+ examples=["renewed"],
2957
+ ),
2958
+ ] = None
2959
+ entity_type: Annotated[
2960
+ str,
2961
+ Field(
2962
+ alias="entityType",
2963
+ description="Type of the resource this event relates to",
2964
+ examples=["order"],
2965
+ ),
2966
+ ]
2967
+ entity_id: Annotated[
2968
+ str,
2969
+ Field(
2970
+ alias="entityId",
2971
+ description="ID of the resource this event relates to",
2972
+ examples=["order_Hn5xWqVfKm8RjTgYbUcP"],
2973
+ ),
2974
+ ]
2975
+ object: Annotated[
2976
+ Order
2977
+ | Chargeback
2978
+ | Refund
2979
+ | Subscription
2980
+ | Checkout
2981
+ | OneOffProduct
2982
+ | SubscriptionPlan
2983
+ | Webhook,
2984
+ Field(
2985
+ description="The full resource payload at the time of the event, serialized identically\nto the corresponding REST resource. The concrete shape is selected by the\npayload's own `resource` field (the discriminator):\n - `order.paid`, `order.canceled`, `order.payment_failed` → Order\n - `order.chargeback_received`, `order.chargeback_reversed` → Chargeback\n - `refund.*` → Refund\n - `subscription.*` → Subscription\n - `checkout.*` → Checkout\n - `webhook.setup` → Webhook (the endpoint config; verification handshake)\n - `one_off_product.*` → OneOffProduct\n - `subscription_plan.*` → SubscriptionPlan\n\nNote `object.resource` may differ from `entityType`: chargeback events carry\n`entityType: order` (the order the chargeback belongs to) but a Chargeback object.\n",
2986
+ discriminator="resource",
2987
+ ),
2988
+ ]
2989
+ created_at: Annotated[
2990
+ AwareDatetime,
2991
+ Field(
2992
+ alias="createdAt",
2993
+ description="ISO 8601 timestamp of when the webhook event was created.",
2994
+ examples=["2026-05-21T14:32:08+00:00"],
2995
+ ),
2996
+ ]
2997
+ testmode: Annotated[
2998
+ bool,
2999
+ Field(
3000
+ description="Whether the event originated from a test-mode resource.\n`true` for events created with a `test_` prefixed token, `false` for live events.\n",
3001
+ examples=[False],
3002
+ ),
3003
+ ]
3004
+ links: Annotated[
3005
+ Links10 | None,
3006
+ Field(
3007
+ description="HATEOAS links to related resources. Present on event deliveries; omitted on\nthe `webhook.setup` verification call, which is not a persisted resource.\n"
3008
+ ),
3009
+ ] = None