poli-page 0.9.0 → 0.11.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da205dffa69777d3da13ab521d2390d2dd1e06474ce6851f99d806524f7efd19
4
- data.tar.gz: bdbfb7093b0c615e2ba62910e8acd8a5fbf269d303b725ee92d35e01ac1d32d5
3
+ metadata.gz: 0caa9c1105af62976da1ba1a5a6ef40ad34386359f1b43cea033976146054d6c
4
+ data.tar.gz: ed1c09e7c54ab2ee34d9b6286a674c755ef6a1450b53569490be006939c3ae0d
5
5
  SHA512:
6
- metadata.gz: 79325da9ac0918bab1e86de4daf459493c1bc5897fc63413905688f01c6c3d130891af2682746a138414591bbf4dcda3e4ef241ba478383f01b13063c02d5075
7
- data.tar.gz: 8c7c5d9389e5facab3da4552256d3a6b7206687d9811cc321a37f45d434c4c2ae4fc73570b3055554cc43ae49403d691d50059e6b50d8c6606dcef18928e582a
6
+ metadata.gz: 779392d70b3ca9aa042d5e935f5d24d9da8d4eede920f43dfb656907107f924672aa644744a0edceb3950538118d99ff3b8fd24ebef274fa2da27d400dc3b68d
7
+ data.tar.gz: 0ddcc5b1c996fb19e51c60025a9353412a6f3f198e84d263a3385eff435d659e303828795efb5185067e2bf8383fb88218b56928e640f35d70cecc3046c38e06
data/CHANGELOG.md CHANGED
@@ -5,11 +5,239 @@ here.
5
5
 
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
- Breaking changes between major versions are summarized in
8
+ Breaking releases (minor bumps while on 0.x) are summarized in
9
9
  [MIGRATION.md](MIGRATION.md).
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.11.0] - 2026-10-30
14
+
15
+ Conformance with the Poli Page API contract v1.3.0
16
+ ([poli-page/api-contract](https://github.com/poli-page/api-contract)). See
17
+ [MIGRATION.md](MIGRATION.md#from-0100-to-0110).
18
+
19
+ ### Changed
20
+
21
+ - **BREAKING**: the keys inside `data:` and `metadata:` are no longer
22
+ renamed. They are sent exactly as you write them, at every depth: before,
23
+ every key was camelized (`data: { invoice_number: 1 }` reached the template
24
+ as `invoiceNumber`). `DocumentDescriptor#metadata` is returned exactly as
25
+ the API stored it, with its String keys: before, every key was snake_cased
26
+ and symbolized (`{ "customerId" => "c" }` came back as
27
+ `{ customer_id: "c" }`), and two keys such as `someKey` and `some_key`
28
+ collided. The SDK's own names (`project`, `template`, the descriptor's
29
+ readers, the thumbnail options…) keep their Ruby spelling. See
30
+ [MIGRATION.md](MIGRATION.md#payload-keys-are-no-longer-renamed).
31
+ - **BREAKING**: `QUOTA_EXCEEDED` and `OVERAGE_CAP_EXCEEDED` (429) and
32
+ `STORAGE_ACCESS_DENIED` (502) are no longer retried, even with a
33
+ `Retry-After` header, and `retryable?` returns `false` for them (it
34
+ returned `true` for any 429 or 5xx). They last until the monthly reset or
35
+ until the bucket access is restored, so the SDK used to wait up to 30 s per
36
+ retry for nothing. `RATE_LIMIT_EXCEEDED` and every other 429 and 5xx are
37
+ still retried. Code that requeues on `retryable?` stops requeueing these
38
+ three.
39
+ - **BREAKING**: `retryable?` is `false` for every `DOWNLOAD_FAILED`
40
+ (`PoliPage::DownloadError`), whatever the storage status: it was `true`
41
+ with a storage 5xx or 429. The SDK never retried the download itself, and
42
+ still does not.
43
+ - **BREAKING**: the error predicates read `status` and `code`, as the SDK
44
+ specification defines them, instead of the error class: `auth_error?` is
45
+ `status` 401 or 403, `rate_limit_error?` 429, `validation_error?` 400, and
46
+ `network_error?` is `code` `network_error` or `timeout`. For API errors
47
+ only the never-retried codes above change. A `DownloadError` now follows
48
+ the storage's status: a 401 or 403 is an auth error (an expired presigned
49
+ URL gets 403), a 429 a rate-limit error and a 400 a validation error; all
50
+ three were `false` before.
51
+ - **BREAKING**: SDK error messages are the exact texts of the SDK
52
+ specification, the same in every Poli Page SDK:
53
+ - `invalid_options`: `apiKey is required` (was `api_key is required`).
54
+ - `network_error` (`ConnectionError`): `Network request failed` (was the
55
+ HTTP library's exception text, which stays available as `cause`).
56
+ - `timeout` (`TimeoutError`): `Request timed out after <ms>ms`, the
57
+ client's `timeout` in milliseconds (was `request timed out after <s>s`).
58
+ `TimeoutError#timeout` is still in seconds.
59
+ - `DOWNLOAD_FAILED`: `Failed to download PDF: HTTP <status>` (was
60
+ `Failed to download: <status> <reason phrase>`), and
61
+ `Failed to download PDF: network error` when the download gets no
62
+ complete answer (was the HTTP library's text, now `cause`).
63
+ - An error answer whose body is not JSON: `HTTP <status>` (was
64
+ `HTTP <status>: response body was not valid JSON`); the code is still
65
+ `INTERNAL_ERROR` with the real status.
66
+ - `DownloadError.new` takes `status:` and `cause:` and builds its message
67
+ from them; `message:` is optional. `ConnectionError.new`'s `message:` is
68
+ optional too.
69
+ - **BREAKING**: `format:` and `orientation:` are no longer checked before the
70
+ request (SDK specification §5.4: the SDK validates nothing else
71
+ client-side). A value outside `PoliPage::PageFormat::FORMATS` or
72
+ `portrait`/`landscape` is sent as given, and the API answers it with
73
+ `PoliPage::ValidationError` (`400 VALIDATION_ERROR`) instead of the SDK
74
+ raising `PoliPage::InvalidOptionsError` (`invalid_options`, which the
75
+ specification keeps for the constructor). `PageFormat.valid?` and
76
+ `Orientation.valid?` stay for callers who check their own input.
77
+ - `render.document`, `render.pdf`, `render.pdf_stream` and `render_to_file`
78
+ raise `PoliPage::InvalidOptionsError` with code
79
+ `PROJECT_REQUIRED_FOR_DOCUMENT` and message `project is required` before
80
+ any request when `project:` is nil or empty (it was sent, and the API
81
+ answered `400`).
82
+ - `Retry-After` is read as integer seconds or an HTTP-date (IMF-fixdate,
83
+ RFC 850 or asctime) only, clamped to 0–30 s; a date's delay keeps its
84
+ sub-second rest instead of being rounded down to whole seconds. Any other
85
+ value (`1.5`, `-1`, `soon`) falls back to the backoff. `Retry-After-Ms` is
86
+ ignored, as before.
87
+ - The client's `timeout` is one deadline for each whole attempt: opening the
88
+ connection (DNS, TCP, a proxy's `CONNECT`, the TLS handshake), the request
89
+ and the answer, body included. Before, it applied to each phase and each
90
+ socket operation on its own, so a slow connection followed by a slow TLS
91
+ handshake, or an answer that kept sending a byte now and then, could
92
+ outlive it while the error still named `timeout`. The connection phase is
93
+ now bounded by the time left, and a per-attempt watchdog thread closes the
94
+ connection at the deadline.
95
+ - An answer's whole body is read inside its attempt. A 2xx cut mid-body is a
96
+ `network_error` (`Network request failed`, the read error as `cause`), and
97
+ one that stalls past `timeout` a `timeout`; both are retried. A 2xx body
98
+ shorter than its `Content-Length` used to be accepted, then failed as
99
+ `INTERNAL_ERROR` ("response body was not valid JSON"). A render whose 2xx
100
+ answer is cut was already stored by the API, so its retry can store a
101
+ second document.
102
+ - An error answer whose body fails while it is read is handled as a body
103
+ that is not JSON: `INTERNAL_ERROR`, `HTTP <status>`, the real status and
104
+ `x-request-id`, the read error as `cause`, and retried by its status (a 503
105
+ yes, a 400 no). It never takes a code from the bytes received so far.
106
+ - `on_response` fires once the 2xx body is read, and never for a 2xx whose
107
+ body failed.
108
+ - `PoliPage::Error#initialize` takes `cause:`; `Error#cause` returns it even
109
+ when the error is raised outside the `rescue` that caught the original.
110
+ Network errors, timeouts and download failures carry the HTTP library's
111
+ exception there.
112
+ - `documents.preview` returns its HTML as a UTF-8 string, read in the
113
+ `charset` of its `Content-Type` (UTF-8 when there is none). It was binary,
114
+ `ASCII-8BIT`, so any non-ASCII character compared unequal to a UTF-8
115
+ string.
116
+ - A 2xx whose body is not a JSON object (not JSON, empty, absent, or another
117
+ JSON value) still raises `INTERNAL_ERROR`, not retried, but now fires
118
+ `on_error` first, like every error the SDK raises, and keeps the parser's
119
+ error as `cause`. An absent body raised a raw `TypeError` and a JSON array a
120
+ raw `NoMethodError`.
121
+ - `documents.thumbnails` ignores thumbnail fields the SDK does not know,
122
+ instead of raising `ArgumentError`, so the API can add fields.
123
+
124
+ ### Fixed
125
+
126
+ - A GET or DELETE (`documents.get`, `documents.preview`, `documents.delete`)
127
+ and the presigned-URL download are never sent again behind the SDK's
128
+ back. `Net::HTTP` resends an idempotent request once by itself
129
+ (`max_retries`, default 1) after a dropped connection, a read error or a
130
+ read timeout: that attempt was not counted by the SDK's `max_retries` nor
131
+ reported to `on_request`, a timed-out attempt lasted twice the `timeout`
132
+ its error named, and the download, which must not be retried, could be
133
+ fetched twice. The SDK now sets `max_retries = 0` on every connection.
134
+ `Net::HTTP` never resent a POST by itself.
135
+ - A dropped connection (`EOFError`, `IOError`, `Errno::EPIPE`, …) was not
136
+ translated and escaped as the raw Ruby exception, outside the retry loop;
137
+ it is now a retried `network_error`. A dropped or timed-out download is
138
+ `DOWNLOAD_FAILED`.
139
+ - A retry wait is never shorter than its delay: when `sleep` returns early,
140
+ the SDK sleeps again for the rest, on the monotonic clock.
141
+ - An error raised by the caller's `pdf_stream` block (for example
142
+ `Errno::ENOSPC` from writing the chunk) propagates unchanged instead of
143
+ being wrapped.
144
+ - Docs: the retries page no longer claims the API deduplicates on
145
+ `Idempotency-Key`. It does not: the key is stable across the retries of one
146
+ call, and a render retried after a late 5xx, a `timeout` or a
147
+ `network_error` can store a second document. The errors docs no longer
148
+ list `QUOTA_EXCEEDED`, `OVERAGE_CAP_EXCEEDED` and `STORAGE_ACCESS_DENIED`
149
+ as retried.
150
+
151
+ ### Added
152
+
153
+ - Known error code `TEMPLATE_UNSAFE` (400: the template, a partial or an SVG
154
+ asset holds an injection attempt; the message names it; not retried), next
155
+ to `TEMPLATE_ERROR`.
156
+ - `spec/contract` runs every SDK case of the API contract against a local
157
+ mock server over real sockets; CI pins the contract by commit
158
+ (`poli-page/api-contract` action) and Dependabot proposes each new version.
159
+ Run it locally with `POLI_CONTRACT=<path to contract.json> bundle exec rspec`.
160
+
161
+ ## [0.10.0] - 2026-10-27
162
+
163
+ Environments contract: an API key carries its environment (`pp_{env}_…`,
164
+ e.g. `pp_sandbox_…`, `pp_live_…`, `pp_staging_…`) and a render uses the
165
+ project's copy published in that environment. Breaking, with no deprecation
166
+ window.
167
+
168
+ ### Removed
169
+
170
+ - **BREAKING**: **`render.preview`** and its **`PreviewResult`** type. The
171
+ API removed `POST /v1/render/preview` (a call now gets `404`).
172
+ `documents.preview(id)` stays.
173
+ - **BREAKING**: **Inline mode**: the `InlineModeInput` type and raw HTML in
174
+ `template:`. `template:` is always a slug.
175
+ - **BREAKING**: The **`version:`** keyword of `render.document`,
176
+ `render.pdf`, `render.pdf_stream` and `render_to_file`, and with it draft
177
+ rendering by an API key. Passing `version:` now raises `ArgumentError`.
178
+ - **BREAKING**: The **`version`** field of `DocumentDescriptor`.
179
+ - **BREAKING**: From `PoliPage::ErrorCodes`: `VERSION_NOT_FOUND`,
180
+ `INVALID_VERSION_FORMAT`, `VERSION_REQUIRED`, `INVALID_VERSION_FOR_KEY_ENV`,
181
+ `MISSING_PROJECT_OR_TEMPLATE`, `GONE` (the API answers `410 DOCUMENT_GONE`)
182
+ and `PAYMENT_REQUIRED` (payment grace is `403 ORGANIZATION_PAYMENT_GRACE`).
183
+
184
+ ### Changed
185
+
186
+ - **BREAKING**: `ProjectModeInput` is renamed **`RenderInput`** and is the
187
+ only input type.
188
+ - The render body is built from the seven documented fields only
189
+ (`project`, `template`, `data`, `metadata`, `format`, `orientation`,
190
+ `locale`); unset optional fields are omitted. The SDK never sends an
191
+ environment: the API resolves it from the key.
192
+ - `DocumentDescriptor#environment` is any environment name (`sandbox`,
193
+ `live`, `staging`, …), returned unchanged. Response fields the SDK does not
194
+ know are ignored instead of failing the `Data.define` constructor.
195
+ - `documents.thumbnails(id)` takes optional options: `width:` is no longer
196
+ required (API default 400) and `format:` accepts `"webp"` (API default
197
+ `"jpeg"`). With no option set the body is `{}`. `ThumbnailOptions.new`
198
+ takes every field as optional.
199
+ - **BREAKING**: `documents.thumbnails(id)` no longer pre-checks `width`,
200
+ `format`, `quality` or `pages`: an out-of-bounds option raises
201
+ `PoliPage::ValidationError` (the API's `400 VALIDATION_ERROR`) instead of
202
+ `PoliPage::InvalidOptionsError` before the request.
203
+ - `documents.delete` is documented as idempotent: the API answers `204` again
204
+ for an already-deleted document, so a re-delete returns `nil`.
205
+ - README, docs site and `examples/demo.rb` follow the environments contract:
206
+ the quick start and the demo render `getting-started`/`welcome` with a
207
+ `sandbox` key and no version, "Authentication & environments" carries the
208
+ key table and the key regex, and previews point to the playground
209
+ (<https://play.poli.page>). The demo checks a pasted key against the key
210
+ regex only and its last step is `not-found` (`404 TEMPLATE_NOT_FOUND`).
211
+ - The nightly integration workflow no longer passes a template version; it
212
+ renders `getting-started`/`welcome` with the `POLI_PAGE_API_KEY` secret (a
213
+ `pp_sandbox_` key).
214
+ - `MIGRATION.md` has a "From 0.9.x to 0.10.0" entry.
215
+
216
+ ### Added
217
+
218
+ - `PoliPage::ErrorCodes`: `PUBLISH_IN_PROGRESS` (503, retried),
219
+ `ENVIRONMENT_MISMATCH` (400), `PROJECT_NOT_IN_ENVIRONMENT` (404),
220
+ `TEMPLATE_NOT_FOUND` (404), `DOCUMENT_GONE` (410), `ENVIRONMENT_PROTECTED`
221
+ (403), `ORGANIZATION_PAYMENT_GRACE` (403), `EMPTY_DOCUMENT` (400),
222
+ `STORAGE_ACCESS_DENIED` (502), `ORGANIZATION_MIGRATING` (503),
223
+ `PROJECT_NOT_FOUND` (404), `TEMPLATE_ERROR` (400),
224
+ `THUMBNAILS_NOT_AVAILABLE` (403), `RATE_LIMIT_EXCEEDED` (429),
225
+ `RENDER_BODY_TOO_LARGE` (413), `DOCUMENT_TOO_LARGE` (413) and
226
+ `RENDER_TIMEOUT` (504): 31 known API codes in all. The list stays open:
227
+ `Error#code` is whatever string the API returns.
228
+ - A render held by a publish of the same project and environment gets
229
+ `503 PUBLISH_IN_PROGRESS` with `Retry-After`; the existing retry policy
230
+ retries it after that delay.
231
+
232
+ ## [0.9.1] - 2026-06-22
233
+
234
+ ### Fixed
235
+
236
+ - Declare `base64` as a runtime dependency. The SDK hands back base64-encoded
237
+ thumbnail data (decoded by consumers with `Base64.decode64`) and ships an
238
+ example that requires `base64`; on Ruby 3.4+, where `base64` is no longer a
239
+ default gem, this surfaced as `LoadError -- base64` without the dependency.
240
+
13
241
  ## [0.9.0] - 2026-06-18
14
242
 
15
243
  ### Added — first release
data/MIGRATION.md CHANGED
@@ -1,14 +1,192 @@
1
1
  # Migration Guide
2
2
 
3
- This file documents breaking changes between major versions of `poli-page`
3
+ This file documents breaking changes between releases of `poli-page`
4
4
  (the Poli Page SDK for Ruby). The SDK follows
5
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes
6
- only ship in major version bumps and always come with an entry here.
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html): while it is on
6
+ 0.x, a breaking change bumps the minor version; from 1.0 on, only a major
7
+ version does. Every breaking release comes with an entry here.
7
8
 
8
- ## 1.0
9
+ ## From 0.10.0 to 0.11.0
9
10
 
10
- The first stable release. No prior versions of `poli-page` were published to
11
- RubyGems — treat `1.0.0` as the starting point.
11
+ The SDK now follows the Poli Page API contract v1.3.0, the same retry policy,
12
+ error predicates and messages as every other Poli Page SDK. No method or
13
+ option is renamed. Code may need a change where it relied on the renaming
14
+ of `data` / `metadata` keys, and in error handling.
15
+
16
+ ### Payload keys are no longer renamed
17
+
18
+ The SDK specification says `metadata` is sent as is and returned unchanged,
19
+ and `data` is the template's own data. The SDK used to camelize every key of
20
+ both on the way out and snake_case (and symbolize) every key of `metadata` on
21
+ the way back. It no longer touches them; only its own names (`project:`,
22
+ `template:`, `doc.page_count`, …) keep their Ruby spelling.
23
+
24
+ - **`data:`** — write the keys your template uses. If it reads
25
+ `invoiceNumber` and you relied on the conversion, send `invoiceNumber`:
26
+
27
+ ```ruby
28
+ # before (converted to invoiceNumber on the wire)
29
+ client.render.pdf(project: "billing", template: "invoice", data: { invoice_number: "INV-001" })
30
+
31
+ # now (sent as written)
32
+ client.render.pdf(project: "billing", template: "invoice", data: { invoiceNumber: "INV-001" })
33
+ ```
34
+
35
+ - **`metadata:`** — sent as written too; send the keys you want stored.
36
+ Reads use the API's String keys:
37
+
38
+ ```ruby
39
+ # before
40
+ doc.metadata[:customer_id]
41
+
42
+ # now
43
+ doc.metadata["customerId"]
44
+ ```
45
+
46
+ Documents rendered before the upgrade keep the keys they were stored with
47
+ (camelCase, since the old SDK converted them).
48
+
49
+ ### Codes that are no longer retried
50
+
51
+ `QUOTA_EXCEEDED`, `OVERAGE_CAP_EXCEEDED` (429) and `STORAGE_ACCESS_DENIED`
52
+ (502) reach you after the first attempt, and `retryable?` is `false` for
53
+ them. `DOWNLOAD_FAILED` is never `retryable?`, whatever the storage status.
54
+ If you requeued on `retryable?`, these no longer requeue: surface the quota
55
+ and the bucket errors instead.
56
+
57
+ ```ruby
58
+ rescue PoliPage::Error => e
59
+ case e.code
60
+ when "QUOTA_EXCEEDED", "OVERAGE_CAP_EXCEEDED" then show_banner("Monthly quota reached.")
61
+ when "STORAGE_ACCESS_DENIED" then alert_ops("Bucket access revoked.")
62
+ else raise unless e.retryable?
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Predicates read `status` and `code`
68
+
69
+ `auth_error?` (401/403), `rate_limit_error?` (429), `validation_error?` (400)
70
+ and `network_error?` (`network_error`/`timeout`) no longer look at the class.
71
+ The answer only changes for `PoliPage::DownloadError`: a storage 403 (an
72
+ expired presigned URL) is now `auth_error?`. Rescuing by class works as
73
+ before.
74
+
75
+ ### Messages
76
+
77
+ Matching on an SDK error's message needs updating; prefer `code`:
78
+
79
+ | Error | Before | Now |
80
+ | --- | --- | --- |
81
+ | `InvalidOptionsError` (no key) | `api_key is required` | `apiKey is required` |
82
+ | `ConnectionError` | the HTTP library's text | `Network request failed` (`cause` keeps the original) |
83
+ | `TimeoutError` | `request timed out after 60s` | `Request timed out after 60000ms` |
84
+ | `DownloadError` (status) | `Failed to download: 403 Forbidden` | `Failed to download PDF: HTTP 403` |
85
+ | `DownloadError` (no answer) | the HTTP library's text | `Failed to download PDF: network error` |
86
+ | non-JSON error body | `HTTP 502: response body was not valid JSON` | `HTTP 502` |
87
+
88
+ ### `format:` and `orientation:` are checked by the API
89
+
90
+ An unknown `format:` or `orientation:` is no longer refused by the SDK
91
+ before the request: the API answers it with `400 VALIDATION_ERROR`.
92
+
93
+ ```ruby
94
+ # before
95
+ rescue PoliPage::InvalidOptionsError # "invalid format: ..."
96
+
97
+ # now
98
+ rescue PoliPage::ValidationError => e # e.code == "VALIDATION_ERROR", e.status == 400
99
+ ```
100
+
101
+ To refuse a bad value before any request, check it yourself with
102
+ `PoliPage::PageFormat.valid?(format)` and `PoliPage::Orientation.valid?(orientation)`.
103
+
104
+ ### `project:` nil or empty
105
+
106
+ `render.*` and `render_to_file` raise `InvalidOptionsError` with code
107
+ `PROJECT_REQUIRED_FOR_DOCUMENT` before any request, instead of sending the
108
+ render and getting a `ValidationError` from the API.
109
+
110
+ ## From 0.9.x to 0.10.0
111
+
112
+ 0.10.0 follows the environments contract: an API key belongs to exactly one
113
+ environment of your organization, its text names that environment
114
+ (`pp_{env}_{32 lowercase hex}`: `pp_sandbox_…`, `pp_live_…`,
115
+ `pp_staging_…`), and a render uses the project's copy published in that
116
+ environment. Nobody used the new contract before this release, so every
117
+ removal ships at once with no deprecation window.
118
+
119
+ ### API keys
120
+
121
+ The `pp_test_` and `pp_sa_` prefixes are gone; standard and service keys
122
+ share the one format above. Create a key in the environment you render in:
123
+ a `sandbox` key replaces a test key (watermarked, not counted toward the
124
+ monthly PDF quota, documents expire after 30 days), a `live` key renders
125
+ unwatermarked, counted documents. The SDK passes the key through unchanged
126
+ and never sends an environment.
127
+
128
+ ### `version:` is removed
129
+
130
+ `render.pdf`, `render.pdf_stream`, `render.document` and `render_to_file`
131
+ no longer take `version:`; passing it raises `ArgumentError` (unknown
132
+ keyword) before any request. Publish the project to the key's environment
133
+ instead of pinning a version. `DocumentDescriptor#version` is removed too.
134
+
135
+ ```ruby
136
+ # 0.9.x
137
+ client.render.pdf(project: "billing", template: "invoice", version: "1.0.0", data: data)
138
+
139
+ # 0.10.0
140
+ client.render.pdf(project: "billing", template: "invoice", data: data)
141
+ ```
142
+
143
+ ### `render.preview` and inline HTML are removed
144
+
145
+ `render.preview`, `PreviewResult` and `InlineModeInput` are gone: the API
146
+ renders published templates only. Preview a template or render inline HTML
147
+ in the playground, <https://play.poli.page>. `documents.preview(id)`, the
148
+ stored HTML of a document you already rendered, stays.
149
+
150
+ ### Types
151
+
152
+ | 0.9.x | 0.10.0 |
153
+ | -------------------------------------- | ---------------------------------------- |
154
+ | `PoliPage::ProjectModeInput` | `PoliPage::RenderInput` |
155
+ | `PoliPage::InlineModeInput` | removed |
156
+ | `PoliPage::PreviewResult` | removed |
157
+ | `DocumentDescriptor#version` | removed |
158
+ | `DocumentDescriptor#environment` (`"sandbox"` or `"live"`) | any environment name (`"sandbox"`, `"live"`, `"staging"`, …) |
159
+
160
+ ### Error codes
161
+
162
+ | 0.9.x code | 0.10.0 |
163
+ | -------------------------------------------------------------- | ------------------------------------------------------------- |
164
+ | `GONE` | `DOCUMENT_GONE` (410) |
165
+ | `PAYMENT_REQUIRED` | `ORGANIZATION_PAYMENT_GRACE` (403) |
166
+ | `VERSION_NOT_FOUND`, `INVALID_VERSION_FORMAT`, `VERSION_REQUIRED`, `INVALID_VERSION_FOR_KEY_ENV` | removed (there is no version); a project or template missing from the key's environment answers `PROJECT_NOT_IN_ENVIRONMENT` or `TEMPLATE_NOT_FOUND` (404) |
167
+ | `MISSING_PROJECT_OR_TEMPLATE` | removed |
168
+
169
+ `PoliPage::ErrorCodes` gains `PROJECT_NOT_FOUND`, `PROJECT_NOT_IN_ENVIRONMENT`,
170
+ `TEMPLATE_NOT_FOUND`, `DOCUMENT_GONE`, `ENVIRONMENT_PROTECTED`,
171
+ `PUBLISH_IN_PROGRESS` and the other codes listed in the CHANGELOG. Code that
172
+ matches on `e.code` should switch to the new names; code that rescues by
173
+ class (`PoliPage::GoneError`, `PoliPage::NotFoundError`, …) keeps working.
174
+
175
+ ### Stored documents
176
+
177
+ - `documents.thumbnails(id)` no longer checks its options on the client: an
178
+ out-of-bounds `width`, `format`, `quality` or `pages` raises
179
+ `PoliPage::ValidationError` (the API's `400 VALIDATION_ERROR`) instead of
180
+ `PoliPage::InvalidOptionsError`. Every option is optional, and
181
+ `format: "webp"` is accepted.
182
+ - `documents.delete(id)` is idempotent: deleting an already-deleted document
183
+ returns `nil` instead of raising `PoliPage::GoneError`. A later
184
+ `documents.get(id)` raises `PoliPage::GoneError` with code `DOCUMENT_GONE`.
185
+
186
+ ## 0.9.0
187
+
188
+ The first release, published to RubyGems on 2026-06-18. The surface below is
189
+ 0.9.x's; see [From 0.9.x to 0.10.0](#from-09x-to-0100) for what changed since.
12
190
 
13
191
  ### Surface
14
192