Errors
Every failure thrown by the SDK is a PoliPage\PoliPageException (which extends RuntimeException). The errorCode property tells you what went wrong; the requestId property is what to quote when filing a support ticket. Specialized subclasses under PoliPage\Exception\* let you catch by HTTP status family.
The predicates read status and errorCode, the same in every Poli Page SDK: isAuthError() is status 401 or 403, isRateLimitError() 429, isValidationError() 400, isNetworkError() is errorCode network_error or timeout, and isRetryable() is a network error, a timeout, a 5xx or a 429, except QUOTA_EXCEEDED, OVERAGE_CAP_EXCEEDED and STORAGE_ACCESS_DENIED; invalid_response and io_error are never retryable, nor is a DOWNLOAD_FAILED, whatever the storage status. A DOWNLOAD_FAILED carries the storage’s status, so a 403 from storage is an auth error.
The exception class
Section titled “The exception class”use PoliPage\PoliPageException;use PoliPage\RenderInput;
try { $client->render->pdf(new RenderInput( project: 'billing', template: 'invoice', data: ['invoiceNumber' => 'INV-001'], ));} catch (PoliPageException $e) { error_log(sprintf('%s %s — %s', $e->errorCode, $e->requestId ?? '', $e->getMessage())); throw $e;}SDK-internal codes
Section titled “SDK-internal codes”These originate inside the SDK before any HTTP call (or in response to a network-layer failure). They are lowercase.
| Code | When | Recovery |
|---|---|---|
invalid_options | Constructor arguments are missing or malformed (empty apiKey, undiscoverable PSR-18 client), or, at request time, apiKey or idempotencyKey is not a valid HTTP header value (it contains CR, LF, NUL or another control character other than tab, such as a trailing newline). The message never contains the value. | Fix the call site. Not retried. |
network_error | TCP/TLS-layer failure reaching the API, including a connection dropped while the answer is read. The message is `Network request failed`; the HTTP library's exception is `getPrevious()`. Surfaced via Exception\ConnectionException. | Automatically retried up to maxRetries times. |
timeout | The request did not complete within the configured `timeout`. The message is `Request timed out after <ms>ms`. Surfaced via Exception\TimeoutException. | Automatically retried up to maxRetries times. Raise `timeout` if it keeps tripping. |
DOWNLOAD_FAILED | The presigned-URL download (`downloadPdf()`, the second hop of `render->pdf()` / `render->pdfStream()`) failed: `Failed to download PDF: HTTP <status>` with the storage `status` (an expired URL gets 403); `Failed to download PDF: timeout` without one when the download outlasted `timeout` (the whole download for `render->pdf()` and `downloadPdf()`, the wait for the first byte or a gap between two chunks for `render->pdfStream()`); `Failed to download PDF: network error` without one otherwise, also when the connection drops while the body or the `pdfStream()` stream is read. | Never retryable, whatever the storage status. Call `documents->get($id)` for a fresh URL — presigned URLs expire after 15 minutes. |
invalid_response | A 2xx answer whose body is not JSON, from a method that expects JSON (the render call, `documents->get()`, `documents->thumbnails()`): usually a proxy or a captive portal between the SDK and the API. The message is `Invalid response: HTTP <status> body is not JSON`; `status` is the real 2xx and `requestId` the answer's. Surfaced via Exception\InvalidResponseException; `toPayload()` answers 502 for it. | Never retried. Check the network path to the API (proxy, `baseUrl`). |
io_error | `renderToFile()` could not write the file: creating a parent directory, opening, writing or moving it into place. No status; the message is `Failed to write PDF: <reason>` with PHP's own error text, and that error is `getPrevious()`. The target file is left untouched. | Never retried. Fix the path, the permissions or the free space. |
PROJECT_REQUIRED_FOR_DOCUMENT | An empty `project`, before any request. The message is `project is required`. | Pass the project slug. |
aborted | A future cancellation primitive aborted the request. | Not retryable. Surface to the caller. |
API codes
Section titled “API codes”Returned by the Poli Page API. Uppercase, snake-case. The SDK forwards the code verbatim and raises the matching PoliPage\Exception\* subclass.
| Code | When | Recovery |
|---|---|---|
INVALID_API_KEY | The API key is malformed or revoked. | Rotate the key in the dashboard. |
MISSING_API_KEY | No API key was provided in the request. | Check the constructor was called with `apiKey`. |
PROJECT_NOT_FOUND | No project with this slug in the organization. | Check the `project` slug. |
PROJECT_NOT_IN_ENVIRONMENT | The project is not published in the key's environment. | Publish the project to that environment, or use a key of an environment it is published in. |
TEMPLATE_NOT_FOUND | The template is absent from the project's copy in the key's environment. | Check the `template` slug; publish the project again if the template is new. |
DOCUMENT_NOT_FOUND | No stored document matches the supplied id. | Check the `documentId`. |
DOCUMENT_GONE | The document was deleted, or is a `sandbox` document older than 30 days. | Render it again. |
VALIDATION_ERROR | The request body failed validation. | Read the error message; fix the input. |
MISSING_DATA | The request body lacks a required `data` field. | Pass `data: []` (or with values) on every render call. |
RATE_LIMIT_EXCEEDED | Too many requests for this key. | Automatically retried; slow down sustained bursts. |
QUOTA_EXCEEDED | The organization's monthly quota is reached (429). | Not retried: it lasts until the monthly reset. Upgrade the tier for more renders. |
OVERAGE_CAP_EXCEEDED | The organization reached its overage cap (429, paid tiers). | Not retried: it lasts until the monthly reset. Raise the cap in the dashboard. |
STORAGE_ACCESS_DENIED | The organization's own storage bucket refuses access (502). | Not retried: restore the bucket role, then call again. |
TEMPLATE_ERROR | The engine rejected the template or its data (400). | Fix the template or the data. |
TEMPLATE_UNSAFE | The template, a partial or an SVG asset holds an injection attempt (400); the message (`detail`) names it. | Not retried. Remove the unsafe content and publish the project again. |
ORGANIZATION_PAYMENT_GRACE | The organization's billing is past due; writes are refused. | Update the payment method in the dashboard. |
PUBLISH_IN_PROGRESS | The project is being published into this environment. | Automatically retried, honouring `Retry-After`. |
INTERNAL_ERROR | The API returned 5xx. Also the code of an error answer whose body is not JSON (a gateway page) or could not be read to its end, with its real status and the message `HTTP <status>`. | Automatically retried when the status is 5xx or 429; contact support if it persists. |
The full list of API codes lives in the reference.
Example
Section titled “Example”<?php
require __DIR__ . '/vendor/autoload.php';
use PoliPage\Exception\RateLimitException;use PoliPage\PoliPage;use PoliPage\PoliPageException;use PoliPage\RenderInput;
$client = PoliPage::client($_ENV['POLI_PAGE_API_KEY']);
try { $pdf = $client->render->pdf(new RenderInput( project: 'billing', template: 'invoice', data: ['invoiceNumber' => 'INV-001'], ));} catch (RateLimitException $e) { // RATE_LIMIT_EXCEEDED outlasted the retries, or QUOTA_EXCEEDED / // OVERAGE_CAP_EXCEEDED, which the SDK never retries: queue the job for later. sleep(30); throw $e;} catch (PoliPageException $e) { throw $e;}