polipage

package module
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 20 Imported by: 0

README

Poli Page SDK for Go

Release Ci Codeql Coverage Go Lint Go.dev Report Deps Docs License

Official Go SDK for Poli Page — render polished PDFs from HTML templates via the Poli Page API.

→ Documentation: https://poli-page.github.io/sdk-go/

→ API reference (auto-generated from doc comments): pkg.go.dev/github.com/poli-page/sdk-go

Install

go get github.com/poli-page/sdk-go

Requires Go 1.25 or later. Zero runtime dependencies.

Quick start

Render the welcome template
package main

import (
    "context"
    "log"
    "os"

    polipage "github.com/poli-page/sdk-go"
    "github.com/poli-page/sdk-go/option"
)

func main() {
    client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))

    pdf, err := client.Render.PDF(context.Background(), polipage.RenderInput{
        Project:  "getting-started",
        Template: "welcome",
        Data:     map[string]any{"name": "SDK Demo"},
    })
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("welcome.pdf", pdf, 0o644); err != nil {
        log.Fatal(err)
    }
}

Run it with a sandbox key: POLI_PAGE_API_KEY=pp_sandbox_x go run . (replace pp_sandbox_x with your key). Every organization gets a getting-started project with a welcome template, published into its sandbox environment when the organization is created, so the snippet runs as-is on a fresh organization. getting-started is a normal, editable project: if your organization renamed it, deleted it or removed it from sandbox, the API answers 404 PROJECT_NOT_FOUND or 404 PROJECT_NOT_IN_ENVIRONMENT. Use a fresh organization in that case.

A render uses the project's copy published in the API key's environment. For your own templates, publish the project to that environment, then swap the slugs:

pdf, err := client.Render.PDF(ctx, polipage.RenderInput{
    Project:  "billing",
    Template: "invoice",
    Data:     map[string]any{"invoiceNumber": "INV-001", "total": 1280},
})

To try a template or raw HTML without an API key, use the playground at play.poli.page.

Write a PDF to disk
err := polipage.RenderToFile(ctx, client, polipage.RenderInput{
    Project:  "getting-started",
    Template: "welcome",
    Data:     map[string]any{"name": "SDK Demo"},
}, "./welcome.pdf")

RenderToFile streams response bytes directly to disk (bounded memory). Parent directories are created on demand; an existing file is replaced. The PDF goes to a temporary file in the same directory, renamed over the target only once it is complete, so a failed render, download or write leaves the target untouched. When the path is a symbolic link, the file it points to is written and the link stays a link. An existing target keeps its permission bits (without setuid, setgid or sticky), but not its owner or group, and other hard links to it keep the old content (the PDF is a new file renamed into place). A read-only target, or one that is not a regular file (a directory, FIFO, socket or device such as /dev/stdout), is refused before any API call; to write to a pipe or stdout, copy Render.PDFStream into it. A file that cannot be written returns Code == ErrCodeIOError (io_error), with the message Failed to write PDF: <path>: <reason> (for example Failed to write PDF: /out/invoice.pdf: permission denied) and the os error as Cause; it is never retryable.

Try it locally — runnable demo

The repo ships a runnable demo that exercises every public method end-to-end against the API:

go run ./cmd/demo
# or remote-only, no checkout:
go run github.com/poli-page/sdk-go/cmd/demo@latest

The demo reads POLI_PAGE_API_KEY and the optional POLI_PAGE_BASE_URL from the shell or ./.env. With no key, it asks for a sandbox key (pp_sandbox_…), checks its format and saves it to .env, so later runs are silent. It renders getting-started/welcome and writes its output to ./output/, in nine steps:

  1. Render.PDF
  2. Render.PDFStream
  3. RenderToFile
  4. Render.Document
  5. Documents.Get
  6. Documents.Thumbnails (the demo carries on if the plan does not include thumbnails)
  7. Documents.Preview
  8. Documents.Delete
  9. not-found: renders template does-not-exist with empty data. The API answers 404 TEMPLATE_NOT_FOUND, and the demo prints the error's code, status, message and request id. Any other result (a success, a network error, another API error) fails the demo.
Stream — for large PDFs or piping to S3 / HTTP responses
body, err := client.Render.PDFStream(ctx, polipage.RenderInput{
    Project:  "billing",
    Template: "invoice",
    Data:     map[string]any{"invoiceNumber": "INV-001"},
})
if err != nil {
    return err
}
defer body.Close()

// Pipe straight to an HTTP response, an S3 upload, a file — anything that
// accepts io.Reader. Memory stays bounded regardless of document size.
_, err = io.Copy(w, body)

The PDF download is bounded by the client's timeout (option.WithTimeout, or option.WithRequestTimeout on the call), on top of ctx: when ctx has its own deadline, the first bound to end wins. For Render.PDF and DownloadPDF it bounds the whole download. For PDFStream it is an idle timeout: it bounds the wait for the storage's answer and each read, not the total, so a large PDF read at your own pace is not cut off. Past it, the call (or the stream's Read) returns ErrCodeDownloadFailed with the message Failed to download PDF: timeout and no status.

Working with stored documents

Every render produces a stored document, accessible via documentId for later download or thumbnails. Render.PDF and Render.PDFStream are conveniences that chain a presigned-URL fetch internally to return bytes; Render.Document returns just the descriptor (skip the auto-download when you'll fetch the bytes later).

// 1. Render and store
doc, err := client.Render.Document(ctx, polipage.RenderInput{
    Project:  "billing",
    Template: "invoice",
    Data:     map[string]any{"invoiceNumber": "INV-001"},
    Metadata: polipage.RenderMetadata{"customerId": "cust_123"}, // your own audit data
})
// doc.DocumentID, doc.Environment, doc.PageCount, doc.SizeBytes, doc.PresignedPDFURL, doc.Metadata, ...

// 2. Save doc.DocumentID in your database
_ = db.Invoices.Update("INV-001", doc.DocumentID)

// 3. Later, fetch a fresh presigned URL + download
fresh, err := client.Documents.Get(ctx, doc.DocumentID)
pdf, err := fresh.DownloadPDF(ctx)

// 4. Generate thumbnails
thumbs, err := client.Documents.Thumbnails(ctx, doc.DocumentID, polipage.ThumbnailOptions{
    Width:  320,
    Format: polipage.ThumbnailFormatPNG,
})

// 5. When done, soft-delete (deleting it again also succeeds)
err = client.Documents.Delete(ctx, doc.DocumentID)

The presigned URL has a ~15-minute TTL. If DownloadPDF fails with Code: ErrCodeDownloadFailed (HTTP 403 from S3), call Documents.Get(ctx, id) to refresh and retry. DownloadPDF is bounded by the client's timeout as a whole; pass option.WithDownloadTimeout(d) to use another bound (0 for none).

Authentication & environments

An API key belongs to one environment of your organization, and its text names that environment: pp_{env}_ followed by 32 lowercase hex characters. A full key matches:

^pp_(?=[a-z][a-z0-9-]{0,19}_)[a-z][a-z0-9]*(?:-[a-z0-9]+)*_[0-9a-f]{32}$
Environment Key Watermark Counted
sandbox (every org) pp_sandbox_… yes no
live (every org, deletable on Pro+) pp_live_… no yes
created on Pro+ (e.g. staging) pp_staging_… no yes

"Counted" means the render counts toward the organization's monthly PDF quota. sandbox documents are watermarked, are not counted and expire 30 days after creation. There is no watermark option: a template that wants a watermark outside sandbox uses the poli-watermark class.

A render uses the project's copy published in the key's environment, and DocumentDescriptor.Environment reports the environment a document was rendered in. Every key uses the same endpoint, https://api.poli.page. The SDK sends the key as a Bearer token and never inspects or validates it.

Methods

Method Returns Description
client.Render.PDF(ctx, in, opts...) ([]byte, error) Render a PDF, return bytes
client.Render.PDFStream(ctx, in, opts...) (io.ReadCloser, error) Render and stream the response
client.Render.Document(ctx, in, opts...) (*DocumentDescriptor, error) Render and return descriptor (skip auto-download)
client.Documents.Get(ctx, id, opts...) (*DocumentDescriptor, error) Retrieve a stored document
client.Documents.Preview(ctx, id, opts...) (*DocumentPreviewResult, error) Stored document's paginated HTML
client.Documents.Thumbnails(ctx, id, options, opts...) ([]Thumbnail, error) Page thumbnails (JPEG/WebP/PNG, base64)
client.Documents.Delete(ctx, id, opts...) error Soft-delete a stored document (a re-delete also succeeds)
(*DocumentDescriptor).DownloadPDF(ctx) ([]byte, error) Fetch bytes from the descriptor's presigned URL
polipage.RenderToFile(ctx, client, in, path) error Render and stream to disk

Configuration

All options live in the option subpackage and are passed to NewClient (variadic). Per-call options (currently only WithIdempotencyKey) are passed to individual methods as the final variadic argument.

Option Type Default Description
option.WithAPIKey(key) string (required) API key; it names its environment (pp_{env}_…)
option.WithBaseURL(url) string https://api.poli.page API base URL
option.WithMaxRetries(n) int 2 Retry budget on top of the initial attempt
option.WithRetryDelay(d) time.Duration 500 * time.Millisecond Base exponential-backoff delay
option.WithTimeout(d) time.Duration 60 * time.Second Timeout of each attempt, on top of ctx's deadline (first to end wins); also bounds the PDF download; 0 or less: no SDK bound
option.WithHTTPClient(c) *http.Client new client Inject custom transport / TLS / middleware
option.WithLogger(l) *slog.Logger discard One DEBUG/attempt, WARN/retry, ERROR/terminal
option.WithOnRetry(fn) func(polipage.RetryEvent) nil Fires before each retry sleep
option.WithOnError(fn) func(error) nil Fires on terminal failure
option.WithIdempotencyKey(key) string UUID4 Per-call — override the auto-generated key
option.WithRequestTimeout(d) time.Duration WithTimeout value Per-call — override the timeout for this call (still on top of ctx's deadline)
option.WithHeader(key, value) string, string — Construction + per-call — extra request header; per-call wins on duplicate keys

Error handling

The SDK returns a single error type, *polipage.Error, for every failure. Use errors.Is against the package-level sentinels for quick branching, or errors.As to inspect the full value:

import (
    "errors"
    "log"

    polipage "github.com/poli-page/sdk-go"
)

pdf, err := client.Render.PDF(ctx, in)
if err != nil {
    if errors.Is(err, polipage.ErrUnauthorized) { return refreshCredentials() } // MISSING_API_KEY or INVALID_API_KEY

    var pErr *polipage.Error
    if errors.As(err, &pErr) {
        if pErr.IsRateLimitError()  { return queueForLater() } // any 429: quota codes are never retried
        if pErr.IsValidationError() { log.Println("bad input:", pErr.Message) }
        if pErr.IsNetworkError()    { log.Println("network/timeout") }
        log.Printf("code=%s status=%d requestId=%s\n", pErr.Code, pErr.StatusCode, pErr.RequestID)
    }
    return err
}

A sentinel matches by error code, not by HTTP status. ErrRateLimit matches QUOTA_EXCEEDED and OVERAGE_CAP_EXCEEDED; to catch every 429, use IsRateLimitError() as above. ErrNotFound matches only the generic NOT_FOUND code, so check the specific codes of a failed render:

var pErr *polipage.Error
if errors.As(err, &pErr) {
    switch pErr.Code {
    case polipage.ErrCodeProjectNotFound:
        return showBanner("No project matches this slug: check it, or whether the project was renamed or deleted.")
    case polipage.ErrCodeProjectNotInEnvironment:
        return showBanner("The project is not published in this key's environment.")
    case polipage.ErrCodeTemplateNotFound:
        return show404()
    case polipage.ErrCodeOrganizationPaymentGrace:
        return showBanner("Billing is past due.")
    case polipage.ErrCodeOrganizationCancelled:
        return showBanner("Subscription cancelled — service is read-only.")
    case polipage.ErrCodeDocumentGone:
        return show410() // the document was deleted or has expired
    }
}

Unknown codes from a newer API pass through verbatim in pErr.Code.

A 2xx answer whose body is not JSON (a proxy or captive portal page between your server and the API) returns Code == ErrCodeInvalidResponse (invalid_response) with the message Invalid response: HTTP <status> body is not JSON, the real 2xx as StatusCode and the answer's x-request-id as RequestID. It is never retried. pErr.ToPayload(), the {code, message, status, requestId} body for framework integrations, answers 502 for it (and 503 for network_error, 504 for timeout), while StatusCode keeps the real status.

Cancellation

Pass a context.Context with a deadline or cancel to abort a render in flight:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

pdf, err := client.Render.PDF(ctx, in)

When the context is canceled, the SDK returns *polipage.Error with Code == ErrCodeAborted, also while the PDF is downloaded or streamed; when the deadline expires, Code == ErrCodeTimeout, except during the PDF download, where it is ErrCodeDownloadFailed with Failed to download PDF: network error (errors.Is(err, context.DeadlineExceeded) still tells it apart). Caller-aborted errors are never retried.

Observability

The SDK ships with log/slog integration as the primary observability mechanism:

logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
client := polipage.NewClient(
    option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
    option.WithLogger(logger),
)

One DEBUG line per HTTP attempt, one WARN per retry, one ERROR per terminal failure. Headers and bodies that could contain the API key are never logged.

For SDK-level events that don't fit a log line, register hooks:

client := polipage.NewClient(
    option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
    option.WithOnRetry(func(e polipage.RetryEvent) {
        metrics.Counter("poli.retry").With("attempt", e.Attempt).Inc()
    }),
    option.WithOnError(func(err error) {
        sentry.CaptureException(err)
    }),
)

Hooks are synchronous, optional, and panic-safe — a hook that panics never breaks the request. For request/response inspection (full headers + body), inject a custom *http.Client via option.WithHTTPClient and add a middleware transport there — see the next section.

Middleware via http.RoundTripper

The SDK's hooks fire at SDK-level events (retry, terminal error). For full-fidelity HTTP middleware — tracing, request signing, response caching, fixture recording, request/response logging with bodies — wrap a http.RoundTripper and pass it via option.WithHTTPClient. This is the stdlib idiom; the SDK does nothing special to enable it.

type tracingTransport struct {
    base http.RoundTripper
}

func (t *tracingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    start := time.Now()
    resp, err := t.base.RoundTrip(req)
    log.Printf("%s %s → %d in %s", req.Method, req.URL.Path, resp.StatusCode, time.Since(start))
    return resp, err
}

client := polipage.NewClient(
    option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
    option.WithHTTPClient(&http.Client{
        Transport: &tracingTransport{base: http.DefaultTransport},
    }),
)

Compose multiple layers by chaining RoundTrippers (each wraps the previous one's base). The SDK's retry loop runs outside this transport, so each RoundTrip corresponds to one attempt — log every retry by counting RoundTrip calls per request.

Per-call tracing IDs

For one-off per-request headers (a tracing ID, a tenant override, a feature flag), use option.WithHeader as a per-call option without rebuilding the client:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

pdf, err := client.Render.PDF(ctx, in,
    option.WithHeader("X-Trace-Id", traceID),
    option.WithRequestTimeout(5*time.Second), // overrides client default for this call only
)

Per-call header keys win over construction-time WithHeader entries for the same key; both override the SDK's own headers (Authorization, Content-Type, etc.) if you supply matching keys — caller's responsibility.

Retries & idempotency

The SDK retries on 5xx, 429, network errors, and timeouts, except QUOTA_EXCEEDED, OVERAGE_CAP_EXCEEDED (429) and STORAGE_ACCESS_DENIED (502), which waiting cannot fix: those are returned at once. Before retry n (1-based) the SDK waits RetryDelay × 2^(n−1) × j, with j a random factor in [0.5, 1.5); a Retry-After header (integer seconds or HTTP-date, clamped to 30 s) replaces it when the server sends one. Retry-After-Ms is ignored. Go's HTTP transport may resend a GET once on a stale keep-alive connection; it never resends a POST. Over HTTP/2, a POST the server refuses before processing it (refused stream, GOAWAY) comes back as network_error and goes through the SDK's own retry; with WithMaxRetries(0) it is returned to you. Every POST sends an auto-generated Idempotency-Key (UUID v4), the same on every retry of that call; the API does not deduplicate on it, so a retried render can store a second document. Pass option.WithIdempotencyKey("…") as the last argument to override:

_, err := client.Render.PDF(ctx, in, option.WithIdempotencyKey("inv-INV-001"))

Type system

  • RenderInput is a struct and the only input of Render.PDF, PDFStream, Document and RenderToFile. Project, Template and Data are required; Format, Orientation, Locale and Metadata are optional and left out of the request when unset. Pass a non-nil Data (an empty map[string]any{} is sent as {}).
  • PageFormat, Orientation and ThumbnailFormat are string types with named constants (PageFormatA4, OrientationLandscape, ThumbnailFormatWebP, …).
  • Nullable wire fields of DocumentDescriptor (ProjectID, TemplateSlug, Orientation, Locale, …) are *string to distinguish JSON null from empty strings. Environment is a plain string: any environment name the API returns.

Concurrency & thread-safety

*polipage.Client is safe for concurrent use. Create one client per process and reuse it across goroutines — the underlying *http.Client pools connections automatically. The SDK has no async/sync split; goroutines + context.Context cover both concurrency and cancellation.

var wg sync.WaitGroup
results := make([]*polipage.DocumentDescriptor, len(invoices))
errs    := make([]error, len(invoices))
for i, inv := range invoices {
    wg.Add(1)
    go func(i int, inv Invoice) {
        defer wg.Done()
        results[i], errs[i] = client.Render.Document(ctx, polipage.RenderInput{
            Project:  "billing",
            Template: "invoice",
            Data:     inv.Data(),
        })
    }(i, inv)
}
wg.Wait()

Runtime support

Server-side only. The SDK runs on:

  • Go 1.25+ — current and N-1 stable Go releases.

Browsers / WebAssembly are not supported. API keys are secrets, whatever their environment, and must never be shipped to a browser. Call the SDK from your backend (HTTP server, CLI, worker, lambda) and proxy the result to the client.

Requirements

Go 1.25 or later. The SDK has zero runtime dependencies — only the Go standard library.

Documentation & support

License

MIT © Poli Page

Documentation

Overview

Package polipage is the official Go SDK for Poli Page.

Construct a client with NewClient, then call methods on the Render or Documents namespaces. The client is safe for concurrent use — share one instance per process so the underlying *http.Client pools connections automatically.

Index

Examples

Constants

View Source
const (
	// ErrCodeInvalidOptions marks constructor or per-call option validation
	// failures (empty API key, malformed metadata, etc.).
	ErrCodeInvalidOptions = "invalid_options"
	// ErrCodeNetworkError marks DNS / connection / TLS failures.
	ErrCodeNetworkError = "network_error"
	// ErrCodeTimeout marks a per-request deadline being exceeded.
	ErrCodeTimeout = "timeout"
	// ErrCodeAborted marks caller-initiated cancellation (a canceled
	// context.Context). Never retryable.
	ErrCodeAborted = "aborted"
	// ErrCodeUnknownError is the fallback code when an error-body JSON has
	// no recognised fields.
	ErrCodeUnknownError = "unknown_error"
	// ErrCodeDownloadFailed marks a failure fetching a presigned PDF URL:
	// a non-2xx storage answer (with its status), a network failure, or a
	// download that outlasted the timeout (no status). Never retryable.
	ErrCodeDownloadFailed = "DOWNLOAD_FAILED"
	// ErrCodeInvalidResponse marks a 2xx API answer whose body is not JSON,
	// from a method that expects JSON (the render call, Documents.Get,
	// Documents.Thumbnails). StatusCode is the real 2xx; the JSON syntax
	// error is wrapped in Cause. Usually a proxy or captive portal between
	// the SDK and the API. Never retried.
	ErrCodeInvalidResponse = "invalid_response"
	// ErrCodeInternalError is the SDK-side fallback when the response body
	// is unparseable or missing. Distinct from ErrCodeAPIInternalError, which
	// has the same wire value but originates from the API.
	ErrCodeInternalError = "INTERNAL_ERROR"
	// ErrCodeIOError marks a file [RenderToFile] could not write (creating
	// the parent directory, the temporary file, writing it or renaming it
	// into place). No status; never retried. The underlying os error is
	// wrapped in Cause — use errors.As(*os.PathError) to inspect.
	ErrCodeIOError = "io_error"
)

SDK-internal error codes. These do not appear in API responses; the SDK sets them on errors raised before, around, or after the HTTP call.

View Source
const (
	ErrCodeMissingAPIKey              = "MISSING_API_KEY" //nolint:gosec // G101: error-code identifier, not a credential
	ErrCodeInvalidAPIKey              = "INVALID_API_KEY" //nolint:gosec // G101: error-code identifier, not a credential
	ErrCodeForbidden                  = "FORBIDDEN"
	ErrCodeOrganizationCancelled      = "ORGANIZATION_CANCELLED"
	ErrCodeOrganizationPurged         = "ORGANIZATION_PURGED"
	ErrCodeOrganizationPaymentGrace   = "ORGANIZATION_PAYMENT_GRACE"
	ErrCodeOrganizationMigrating      = "ORGANIZATION_MIGRATING"
	ErrCodeEnvironmentMismatch        = "ENVIRONMENT_MISMATCH"
	ErrCodeEnvironmentProtected       = "ENVIRONMENT_PROTECTED"
	ErrCodeNotFound                   = "NOT_FOUND"
	ErrCodeProjectNotFound            = "PROJECT_NOT_FOUND"
	ErrCodeProjectNotInEnvironment    = "PROJECT_NOT_IN_ENVIRONMENT"
	ErrCodeTemplateNotFound           = "TEMPLATE_NOT_FOUND"
	ErrCodeDocumentNotFound           = "DOCUMENT_NOT_FOUND"
	ErrCodeDocumentGone               = "DOCUMENT_GONE"
	ErrCodeValidationError            = "VALIDATION_ERROR"
	ErrCodeMissingData                = "MISSING_DATA"
	ErrCodeMissingTemplateSlug        = "MISSING_TEMPLATE_SLUG"
	ErrCodeProjectRequiredForDocument = "PROJECT_REQUIRED_FOR_DOCUMENT"
	ErrCodeTemplateError              = "TEMPLATE_ERROR"
	ErrCodeTemplateUnsafe             = "TEMPLATE_UNSAFE"
	ErrCodeEmptyDocument              = "EMPTY_DOCUMENT"
	ErrCodeRenderBodyTooLarge         = "RENDER_BODY_TOO_LARGE"
	ErrCodeDocumentTooLarge           = "DOCUMENT_TOO_LARGE"
	ErrCodeThumbnailsNotAvailable     = "THUMBNAILS_NOT_AVAILABLE"
	ErrCodeQuotaExceeded              = "QUOTA_EXCEEDED"
	ErrCodeOverageCapExceeded         = "OVERAGE_CAP_EXCEEDED"
	ErrCodeRateLimitExceeded          = "RATE_LIMIT_EXCEEDED"
	// ErrCodePublishInProgress is a 503 answered while a publish locks the
	// project in the key's environment. Retried like any 5xx, honouring
	// Retry-After.
	ErrCodePublishInProgress   = "PUBLISH_IN_PROGRESS"
	ErrCodeStorageAccessDenied = "STORAGE_ACCESS_DENIED"
	ErrCodeRenderTimeout       = "RENDER_TIMEOUT"
	// ErrCodeAPIInternalError shares the wire value "INTERNAL_ERROR" with
	// ErrCodeInternalError; kept as a separate constant for call-site clarity.
	ErrCodeAPIInternalError = "INTERNAL_ERROR"
)

Known API error codes pass through verbatim from the deployed API. Callers may see codes not in this list — the SDK preserves whatever the API returns.

View Source
const (
	// DefaultBaseURL is the production Poli Page API base URL. Override via
	// option.WithBaseURL to target another API host.
	DefaultBaseURL = "https://api.poli.page"

	// DefaultMaxRetries is the default retry budget: 2 retries on top of the
	// initial attempt, so 3 attempts total before a retryable error surfaces.
	DefaultMaxRetries = 2

	// DefaultRetryDelay is the base exponential-backoff delay. The first
	// retry waits ~DefaultRetryDelay × jitter, the second ~2×, etc.
	DefaultRetryDelay = 500 * time.Millisecond

	// DefaultTimeout is the timeout of each API attempt and of the PDF
	// download, applied on top of the caller's context deadline (the first
	// to end wins).
	DefaultTimeout = 60 * time.Second
)

User-facing default values for client construction. Internal SDK values (header names, jitter bounds, path templates) live in internal/constants instead.

Variables

View Source
var (
	ErrUnauthorized          = &Error{Code: ErrCodeMissingAPIKey}         // also matches INVALID_API_KEY
	ErrForbidden             = &Error{Code: ErrCodeForbidden}             //
	ErrNotFound              = &Error{Code: ErrCodeNotFound}              //
	ErrDocumentNotFound      = &Error{Code: ErrCodeDocumentNotFound}      //
	ErrDocumentGone          = &Error{Code: ErrCodeDocumentGone}          // 410 — document deleted or expired
	ErrValidation            = &Error{Code: ErrCodeValidationError}       //
	ErrRateLimit             = &Error{Code: ErrCodeQuotaExceeded}         // also matches OVERAGE_CAP_EXCEEDED
	ErrTimeout               = &Error{Code: ErrCodeTimeout}               //
	ErrAborted               = &Error{Code: ErrCodeAborted}               //
	ErrNetwork               = &Error{Code: ErrCodeNetworkError}          //
	ErrDownloadFailed        = &Error{Code: ErrCodeDownloadFailed}        //
	ErrInvalidResponse       = &Error{Code: ErrCodeInvalidResponse}       // 2xx API answer whose body is not JSON
	ErrIOError               = &Error{Code: ErrCodeIOError}               // RenderToFile could not write the file
	ErrOrganizationCancelled = &Error{Code: ErrCodeOrganizationCancelled} // 403 — subscription cancelled, service is read-only
	ErrOrganizationPurged    = &Error{Code: ErrCodeOrganizationPurged}    // 410 — organization data has been purged
)

Sentinel errors. Use with errors.Is for ergonomic call-site checks:

if errors.Is(err, polipage.ErrNotFound) {
    return nil // treat 404 as absent without further inspection
}

Each sentinel is a zero-status *Error keyed by Code; (*Error).Is matches receivers whose Code equals the sentinel's, with two logical groups (auth, rate-limit) that compare equal across related codes.

Functions

func RenderToFile

func RenderToFile(ctx context.Context, c *Client, in RenderInput, path string) error

RenderToFile renders a PDF and streams the bytes to path. Parent directories are created if missing; an existing file at path is replaced. Memory-bounded — streaming via io.Copy regardless of PDF size.

When path is a symbolic link (or a chain of them), the file it points to is written and the link stays a link. The PDF is written to a temporary file next to that file, which is renamed over it only once the whole PDF is written. When anything fails (the render, the download or the write), the target is left untouched: an existing file keeps its content and a missing one is not created. The directory and the temporary file are prepared before the render, so a path that cannot be written costs no render.

An existing target keeps its permission bits, without setuid, setgid or sticky. Because the PDF replaces it with a new file, its owner and group become those of the calling process, and other hard links to it keep the old content. An existing target that is not writable (a read-only file) or not a regular file (a directory, FIFO, socket or device such as /dev/stdout) is not replaced: RenderToFile fails before any API call, with the reason "not a regular file" for the latter. To write the PDF to a pipe or to stdout, copy Render.PDFStream into it instead.

Errors from the render call and the download propagate as-is (typed *Error, see Render.PDFStream). A file that cannot be written surfaces as *Error with Code == ErrCodeIOError, no status, the message "Failed to write PDF: <path>: <reason>", where <reason> is the os error's own text (for example "Failed to write PDF: /out/invoice.pdf: permission denied"), and that error wrapped in Cause. It is never retryable.

Callers should reuse a single *Client across calls. RenderToFile is a free function because it is a thin convenience over Render.PDFStream that makes no other network call.

Example

ExampleRenderToFile streams a PDF directly to disk. Parent directories are created if missing; existing files are overwritten. Memory stays bounded regardless of document size.

package main

import (
	"context"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	err := polipage.RenderToFile(
		context.Background(),
		client,
		polipage.RenderInput{
			Project: "billing", Template: "invoice",
			Data: map[string]any{"invoiceNumber": "INV-001"},
		},
		"out/invoice.pdf",
	)
	if err != nil {
		log.Fatal(err)
	}
}

Types

type Client

type Client struct {

	// Render is the render namespace. See [Render] for available methods.
	Render *Render

	// Documents is the stored-documents namespace. See [Documents] for
	// available methods.
	Documents *Documents
	// contains filtered or unexported fields
}

Client is the Poli Page SDK entry point. Construct one via NewClient and reuse it for the lifetime of the process.

func NewClient

func NewClient(opts ...option.RequestOption) *Client

NewClient constructs a Poli Page SDK client. Options are applied in order; later options override earlier ones for the same field.

Validation is deferred: NewClient never returns an error. If a required option is missing (e.g. option.WithAPIKey), the first method call returns an *Error with Code == ErrCodeInvalidOptions.

client := polipage.NewClient(
    option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
)
Example

ExampleNewClient demonstrates the canonical construction of a Client. The API key is the only required option; sensible defaults (3 attempts total, 500ms base retry delay, 60s per-request timeout) apply to everything else.

package main

import (
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(
		option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
		option.WithMaxRetries(3),
	)
	// Share one client across the process — *http.Client pools
	// connections automatically.
	_ = client
}
Example (WithLogger)

ExampleNewClient_withLogger wires a structured slog logger. The SDK emits one DEBUG line per HTTP attempt, one WARN per retry, and one ERROR per terminal failure. Headers and bodies that could contain the API key are not logged.

package main

import (
	"log/slog"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
	client := polipage.NewClient(
		option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")),
		option.WithLogger(logger),
	)
	_ = client
}
Example (WithTimeout)

ExampleNewClient_withTimeout shows the recommended cancellation / deadline pattern: a per-call context.WithTimeout. The Client's own WithTimeout bounds each attempt on top of it: the first to end wins.

package main

import (
	"context"
	"os"
	"time"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	_, _ = client.Render.PDF(ctx, polipage.RenderInput{
		Project: "billing", Template: "invoice",
		Data: map[string]any{},
	})
}

type DocumentDescriptor

type DocumentDescriptor struct {
	DocumentID     string `json:"documentId"`
	OrganizationID string `json:"organizationId"`

	ProjectID    *string `json:"projectId"`
	ProjectSlug  *string `json:"projectSlug"`
	TemplateID   *string `json:"templateId"`
	TemplateSlug *string `json:"templateSlug"`
	APIKeyID     *string `json:"apiKeyId"`

	// Environment is the name of the environment the document was rendered
	// in ("sandbox", "live", or any other environment of the organization),
	// returned as the API sent it.
	Environment string     `json:"environment"`
	Format      PageFormat `json:"format"`

	Orientation *string `json:"orientation"`
	Locale      *string `json:"locale"`

	PageCount int   `json:"pageCount"`
	SizeBytes int64 `json:"sizeBytes"`

	CreatedAt string `json:"createdAt"`
	ExpiresAt string `json:"expiresAt"`

	// Metadata is the caller-supplied metadata echoed by the server. Always
	// non-nil after a successful decode — a server null is normalised to an
	// empty map so callers can range without a nil check.
	Metadata RenderMetadata `json:"metadata"`

	// PresignedPDFURL has a ~15-minute TTL. Re-fetch via Documents.Get when
	// expired.
	PresignedPDFURL string `json:"presignedPdfUrl"`
	// contains filtered or unexported fields
}

DocumentDescriptor is a stored document returned by Render.Document and Documents.Get. Top-level fields are system-controlled; Metadata echoes caller-supplied data.

Nullable wire fields (ProjectID, ProjectSlug, TemplateID, TemplateSlug, APIKeyID, Orientation, Locale) are *string so the SDK can distinguish JSON null from an empty string.

func (*DocumentDescriptor) DownloadPDF

func (d *DocumentDescriptor) DownloadPDF(ctx context.Context, opts ...option.DownloadOption) ([]byte, error)

DownloadPDF fetches the PDF bytes from PresignedPDFURL. The URL has a ~15-minute TTL — when it expires, call Documents.Get(id) to refresh and retry.

The whole download, body included, is bounded by option.WithDownloadTimeout, else the client's option.WithTimeout, on top of ctx: the first bound to end wins. A non-2xx answer returns *Error with Code == ErrCodeDownloadFailed and the storage status; a network failure, before the answer or while the body is read (a body that ends short of its Content-Length included), returns ErrCodeDownloadFailed without a status, and so does a download that outlasts the timeout, with the message "Failed to download PDF: timeout". Canceling ctx returns ErrCodeAborted. The underlying error is wrapped in Cause.

The request is unauthenticated and is not subject to the SDK's retry policy: presigned-URL fetches go directly to S3 (or equivalent). A DOWNLOAD_FAILED is never retryable.

Example

ExampleDocumentDescriptor_DownloadPDF fetches the bytes from a descriptor's PresignedPDFURL. Useful when you persisted the descriptor earlier and want to defer the byte fetch until needed.

package main

import (
	"context"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	doc, err := client.Documents.Get(context.Background(), "doc_abc123")
	if err != nil {
		log.Fatal(err)
	}
	pdf, err := doc.DownloadPDF(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	_ = os.WriteFile("invoice.pdf", pdf, 0o644)
}

type DocumentPreviewResult

type DocumentPreviewResult struct {
	HTML      string
	PageCount int
}

DocumentPreviewResult is the result of Documents.Preview — the paginated HTML for an already-stored document and the page count the engine emitted at render time.

type Documents

type Documents struct {
	// contains filtered or unexported fields
}

Documents is the stored-documents namespace exposed as client.Documents. It hosts the four lifecycle methods defined in spec §6.

func (*Documents) Delete

func (d *Documents) Delete(ctx context.Context, id string, opts ...option.RequestOption) error

Delete soft-deletes a stored document. The PDF is purged from storage; metadata is retained for audit. Deleting an already-deleted document also succeeds: the API answers 204 either way.

Spec §6.4.

Example

ExampleDocuments_Delete soft-deletes a stored document. Deleting it again also succeeds; subsequent reads of the same documentId surface as polipage.ErrDocumentGone.

package main

import (
	"context"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	if err := client.Documents.Delete(context.Background(), "doc_abc123"); err != nil {
		log.Fatal(err)
	}
}

func (*Documents) Get

Get retrieves a stored document's descriptor with a fresh presigned URL. The returned descriptor's DocumentDescriptor.DownloadPDF uses the parent client for transport; calling it after the URL's ~15-minute TTL returns *Error{Code: ErrCodeDownloadFailed} — re-fetch via Get.

Spec §6.1.

Example

ExampleDocuments_Get retrieves a stored document with a fresh presigned URL. The PDF download URL has a ~15-minute TTL — call Get again to reissue it once expired.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	doc, err := client.Documents.Get(context.Background(), "doc_abc123")
	if err != nil {
		log.Fatal(err)
	}
	pdf, err := doc.DownloadPDF(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d bytes\n", len(pdf))
}

func (*Documents) Preview

Preview retrieves a stored document's paginated HTML. The engine does no work — this is a cheap read.

The deployed API returns the HTML as text/html (not a JSON envelope) and the page count via the X-Document-Page-Count response header. Missing or unparseable headers are tolerated: PageCount defaults to 0.

Spec §6.2.

Example

ExampleDocuments_Preview retrieves the stored HTML for a document. The engine performs no work on this call — it's a cheap read suitable for preview UIs and snapshot tests in CI.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	res, err := client.Documents.Preview(context.Background(), "doc_abc123")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d page(s), %d chars HTML\n", res.PageCount, len(res.HTML))
}

func (*Documents) Thumbnails

func (d *Documents) Thumbnails(ctx context.Context, id string, options ThumbnailOptions, opts ...option.RequestOption) ([]Thumbnail, error)

Thumbnails generates page thumbnails for a stored document. Every option is optional: pass a zero ThumbnailOptions for the API defaults (400 px wide JPEG, every page).

The deployed API expects the options wrapped under a top-level "thumbnails" key on the wire and returns the array under the same key — both are handled here so callers see a plain slice. With no option set the body is {}.

Spec §6.3.

Example

ExampleDocuments_Thumbnails generates page thumbnails. Every option is optional: a zero polipage.ThumbnailOptions gets the API defaults (400 px wide JPEG, every page). Tier-gated on the API — Free tier returns THUMBNAILS_NOT_AVAILABLE.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	thumbs, err := client.Documents.Thumbnails(
		context.Background(),
		"doc_abc123",
		polipage.ThumbnailOptions{
			Width:  840,
			Format: polipage.ThumbnailFormatWebP,
			Pages:  []int{1, 2, 3},
		},
	)
	if err != nil {
		log.Fatal(err)
	}
	for _, t := range thumbs {
		fmt.Printf("page %d: %dx%d %s\n", t.Page, t.Width, t.Height, t.ContentType)
	}
}

type Error

type Error struct {
	// Code is the machine-readable error code. For HTTP errors this is the
	// API's code field (verbatim); for SDK-internal errors it is one of
	// ErrCode* (invalid_options, network_error, timeout, aborted, etc.).
	Code string
	// StatusCode is the HTTP response status. Zero for SDK-internal errors
	// (network failure, timeout, abort, constructor validation).
	StatusCode int
	// Message is the human-readable error message.
	Message string
	// RequestID is the x-request-id header value when present; empty otherwise.
	RequestID string
	// Cause is the underlying error this *Error wraps. nil when the error
	// originated in the SDK itself with no upstream cause.
	Cause error
}

Error is the single error type raised by the SDK: API errors, network failures, timeouts, caller cancellation, download failures, invalid responses, file write failures and constructor validation failures all surface as *Error.

Callers can use either errors.Is against the package-level sentinels (ErrNotFound, ErrUnauthorized, etc.) for quick branching or errors.As to inspect Code, StatusCode, RequestID, and the wrapped Cause.

Example (TypedInspection)

ExampleError_typedInspection demonstrates the two idiomatic ways to branch on errors returned by the SDK: errors.Is against the package-level sentinels for quick checks, and errors.As to inspect the full *Error value (Code, StatusCode, RequestID, Cause).

package main

import (
	"context"
	"errors"
	"fmt"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey("pp_sandbox_bad_key"))
	_, err := client.Render.PDF(context.Background(), polipage.RenderInput{
		Project: "x", Template: "y", Data: map[string]any{},
	})
	if errors.Is(err, polipage.ErrUnauthorized) {
		// refresh credentials, surface to the user, ...
	}
	var pErr *polipage.Error
	if errors.As(err, &pErr) {
		fmt.Printf("code=%s status=%d requestID=%s\n", pErr.Code, pErr.StatusCode, pErr.RequestID)
	}
}

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface. The format is stable but not parseable — callers wanting structured access should use errors.As to extract *Error.

func (*Error) Is

func (e *Error) Is(target error) bool

Is supports errors.Is by matching the receiver against package-level sentinel errors via their Code field. A small number of logical groups (auth: MISSING_API_KEY / INVALID_API_KEY; rate-limit: QUOTA_EXCEEDED / OVERAGE_CAP_EXCEEDED) compare equal so the corresponding sentinel matches either underlying code.

func (*Error) IsAuthError

func (e *Error) IsAuthError() bool

IsAuthError reports whether the response was 401 or 403 — invalid, missing, or unauthorized API key.

func (*Error) IsNetworkError

func (e *Error) IsNetworkError() bool

IsNetworkError reports whether the error is a transport-level failure (DNS, connection refused, TLS) or a per-request timeout. Caller-aborted requests (ErrCodeAborted) and constructor-validation failures (ErrCodeInvalidOptions) do not count.

func (*Error) IsRateLimitError

func (e *Error) IsRateLimitError() bool

IsRateLimitError reports whether the response was 429 — too many requests.

func (*Error) IsRetryable

func (e *Error) IsRetryable() bool

IsRetryable reports whether the SDK considers this error retryable: a network failure, a timeout, a 5xx or a 429, except QUOTA_EXCEEDED, OVERAGE_CAP_EXCEEDED and STORAGE_ACCESS_DENIED, which last until the monthly reset or until the bucket access is restored. A DOWNLOAD_FAILED is never retryable, whatever the storage status. Caller-aborted requests, invalid_response (the same answer would come back) and io_error (the file still cannot be written) are never retryable. The SDK already retries internally up to the configured limit; this predicate is useful when an outer queue or scheduler decides whether to re-enqueue the job.

func (*Error) IsValidationError

func (e *Error) IsValidationError() bool

IsValidationError reports whether the response was 400 — request payload failed validation.

func (*Error) ToPayload

func (e *Error) ToPayload() Payload

ToPayload returns the canonical wire payload for framework integrations. The Error.StatusCode field is never changed: it stays 0 for transport failures and keeps the real 2xx for invalid_response. Only the payload surfaces 503, 504 and 502, so callers reading StatusCode are not affected.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the wrapped Cause to errors.Is and errors.As.

type Orientation

type Orientation string

Orientation is the page orientation: portrait or landscape.

const (
	OrientationPortrait  Orientation = "portrait"
	OrientationLandscape Orientation = "landscape"
)

Canonical orientations.

type PageFormat

type PageFormat string

PageFormat is a canonical Poli Page page format. Values match the strings accepted by the deployed API and shared across every SDK.

const (
	PageFormatA3        PageFormat = "A3"
	PageFormatA4        PageFormat = "A4"
	PageFormatA5        PageFormat = "A5"
	PageFormatA6        PageFormat = "A6"
	PageFormatB4        PageFormat = "B4"
	PageFormatB5        PageFormat = "B5"
	PageFormatLetter    PageFormat = "Letter"
	PageFormatLegal     PageFormat = "Legal"
	PageFormatTabloid   PageFormat = "Tabloid"
	PageFormatExecutive PageFormat = "Executive"
	PageFormatStatement PageFormat = "Statement"
	PageFormatFolio     PageFormat = "Folio"
)

Canonical page formats. See the platform spec for the authoritative list.

type Payload

type Payload struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	Status    int    `json:"status"`
	RequestID string `json:"requestId"`
}

Payload is the canonical wire shape for framework integrations: {code, message, status, requestId}, exactly these four JSON keys. Status is the API HTTP status for status-bearing failures, 503 for network failures, 504 for timeouts, 502 for invalid_response (whose own StatusCode is the real 2xx, which an integration must not answer an error with), and 0 otherwise. In JSON, a zero Status and an empty RequestID are null.

Do not embed Payload in another struct: its MarshalJSON is promoted, so the embedding struct marshals as the Payload alone and its own fields drop out of the JSON. Use a named field instead.

func (Payload) MarshalJSON added in v0.11.0

func (p Payload) MarshalJSON() ([]byte, error)

MarshalJSON writes Status and RequestID as null when they are unknown (zero and empty), as the SDK specification's toPayload() does.

type Render

type Render struct {
	// contains filtered or unexported fields
}

Render is the render namespace exposed as client.Render: PDF, PDFStream and Document, the spec §5 surface.

func (*Render) Document

func (r *Render) Document(ctx context.Context, in RenderInput, opts ...option.RequestOption) (*DocumentDescriptor, error)

Document renders a PDF, stores it server-side, and returns the descriptor. The PDF bytes are NOT fetched — call (*DocumentDescriptor).DownloadPDF when you need them, or use Render.PDF / Render.PDFStream for the one-call shortcut.

The API renders the project's copy published in the API key's environment. Variadic options apply per-call; the most useful one is option.WithIdempotencyKey when the caller has a natural identifier (e.g. an invoice number) and wants to override the auto-generated UUID4.

Example

ExampleRender_Document stores the document server-side and returns its descriptor without auto-fetching the PDF bytes. Use when you want to persist the documentId for later access (preview, thumbnails, re-download) without paying for the bytes up front.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	doc, err := client.Render.Document(context.Background(), polipage.RenderInput{
		Project: "billing", Template: "invoice",
		Data: map[string]any{"invoiceNumber": "INV-001"},
		// Metadata is echoed back on Documents.Get — primitives only.
		Metadata: polipage.RenderMetadata{"customerId": "cust_123"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("documentId=%s pages=%d\n", doc.DocumentID, doc.PageCount)
}

func (*Render) PDF

func (r *Render) PDF(ctx context.Context, in RenderInput, opts ...option.RequestOption) ([]byte, error)

PDF renders a PDF and returns the raw bytes. Two HTTP calls under the hood: POST /v1/render to produce a stored document, then GET PresignedPDFURL to fetch the bytes.

The download is bounded as a whole (body included) by the call's option.WithRequestTimeout, else the client's option.WithTimeout, on top of ctx: the first bound to end wins. Past the SDK's timeout, the call returns ErrCodeDownloadFailed with the message "Failed to download PDF: timeout" and no status.

Example

ExampleRender_PDF renders a template of a project into a []byte, using the project's copy published in the API key's environment. Two HTTP calls under the hood; one from the caller's point of view.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	pdf, err := client.Render.PDF(context.Background(), polipage.RenderInput{
		Project:  "billing",
		Template: "invoice",
		Data:     map[string]any{"invoiceNumber": "INV-001"},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("PDF size: %d bytes\n", len(pdf))
}
Example (WithIdempotencyKey)

ExampleRender_PDF_withIdempotencyKey supplies a natural identifier as the Idempotency-Key so retried requests against the same logical invoice never produce duplicate documents server-side.

package main

import (
	"context"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	_, _ = client.Render.PDF(
		context.Background(),
		polipage.RenderInput{
			Project: "billing", Template: "invoice",
			Data: map[string]any{"invoiceNumber": "INV-001"},
		},
		option.WithIdempotencyKey("inv-INV-001"),
	)
}

func (*Render) PDFStream

func (r *Render) PDFStream(ctx context.Context, in RenderInput, opts ...option.RequestOption) (io.ReadCloser, error)

PDFStream is like Render.PDF but returns the PDF as an io.ReadCloser so the caller can stream it directly to an HTTP response, an S3 upload, or a file without buffering.

The download's timeout (the call's option.WithRequestTimeout, else the client's option.WithTimeout) is an idle timeout, on top of ctx (the first bound to end wins): it bounds the wait for the storage's answer and for each read of the stream, not the total, so a large PDF read at the caller's pace is not cut off. Past it, PDFStream or the stream's Read returns ErrCodeDownloadFailed with the message "Failed to download PDF: timeout".

The caller MUST close the returned ReadCloser when done, even on early return paths.

Example

ExampleRender_PDFStream streams PDF bytes directly to a destination without buffering — memory stays bounded regardless of document size. The caller MUST close the returned ReadCloser.

package main

import (
	"context"
	"io"
	"log"
	"os"

	polipage "github.com/poli-page/sdk-go"
	"github.com/poli-page/sdk-go/option"
)

func main() {
	client := polipage.NewClient(option.WithAPIKey(os.Getenv("POLI_PAGE_API_KEY")))
	body, err := client.Render.PDFStream(context.Background(), polipage.RenderInput{
		Project: "billing", Template: "invoice",
		Data: map[string]any{"invoiceNumber": "INV-001"},
	})
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = body.Close() }()

	out, _ := os.Create("invoice.pdf")
	defer func() { _ = out.Close() }()
	_, _ = io.Copy(out, body)
}

type RenderInput

type RenderInput struct {
	// Required.
	Project  string         `json:"project"`
	Template string         `json:"template"`
	Data     map[string]any `json:"data"`

	// Optional — omitted from the wire when unset.
	Format      PageFormat     `json:"format,omitempty"`
	Orientation Orientation    `json:"orientation,omitempty"`
	Locale      string         `json:"locale,omitempty"`
	Metadata    RenderMetadata `json:"metadata,omitempty"`
}

RenderInput is the input of every render method: a template of a project, rendered with the project's copy published in the API key's environment. Only these seven fields are sent; unset optional fields are omitted from the wire.

type RenderMetadata

type RenderMetadata = map[string]any

RenderMetadata is free-form caller metadata forwarded to the API and echoed on responses that support it. Values must be primitives (string, number, bool); nested maps and slices are rejected at serialise time with *Error{Code: ErrCodeInvalidOptions}.

type RequestEvent

type RequestEvent = clientconfig.RequestEvent

RequestEvent is delivered to the OnRequest hook before each HTTP attempt. Aliased to clientconfig.RequestEvent so option.WithOnRequest and the re-export here both spell the type identically.

type ResponseEvent

type ResponseEvent = clientconfig.ResponseEvent

ResponseEvent is delivered to the OnResponse hook once per 2xx attempt, after its body has been read in full; never for a 2xx whose body read fails. DurationMs runs from just before the request is sent until the body is read. Aliased to clientconfig.ResponseEvent.

type RetryEvent

type RetryEvent = clientconfig.RetryEvent

RetryEvent is delivered to the OnRetry hook before each retry sleep. Aliased to clientconfig.RetryEvent so option.WithOnRetry and the re-export here can both spell the type identically.

type Thumbnail

type Thumbnail struct {
	Page        int    `json:"page"`
	Width       int    `json:"width"`
	Height      int    `json:"height"`
	ContentType string `json:"contentType"`
	Data        string `json:"data"`
}

Thumbnail is a single page thumbnail returned by Documents.Thumbnails. Data is base64-encoded image bytes; the caller decodes via encoding/base64.StdEncoding.DecodeString as needed.

type ThumbnailFormat

type ThumbnailFormat string

ThumbnailFormat is the image format for a generated thumbnail. The API default is JPEG, which also accepts a Quality option.

const (
	ThumbnailFormatJPEG ThumbnailFormat = "jpeg"
	ThumbnailFormatWebP ThumbnailFormat = "webp"
	ThumbnailFormatPNG  ThumbnailFormat = "png"
)

Canonical thumbnail formats.

type ThumbnailOptions

type ThumbnailOptions struct {
	// Width is the thumbnail width in pixels, 50-2000. API default: 400.
	Width int `json:"width,omitempty"`
	// Format selects jpeg, webp or png. API default: jpeg.
	Format ThumbnailFormat `json:"format,omitempty"`
	// Quality is the JPEG quality 1-100. Only valid when Format is JPEG.
	Quality int `json:"quality,omitempty"`
	// Pages selects specific 1-based page numbers. Empty means all pages.
	Pages []int `json:"pages,omitempty"`
}

ThumbnailOptions configures a thumbnail-generation request. Every field is optional: a zero value is not sent and the API applies its default. The SDK does not check bounds; the API answers 400 VALIDATION_ERROR.

Directories

Path Synopsis
cmd
demo command
Demo of the official Poli Page SDK for Go.
Demo of the official Poli Page SDK for Go.
examples
documents-delete command
Demonstrates: client.Documents.Delete(ctx, id) — soft-delete a stored document.
Demonstrates: client.Documents.Delete(ctx, id) — soft-delete a stored document.
documents-get command
Demonstrates: client.Documents.Get(ctx, id) — fetch a stored document.
Demonstrates: client.Documents.Get(ctx, id) — fetch a stored document.
documents-preview command
Demonstrates: client.Documents.Preview(ctx, id) — get a stored document's HTML preview.
Demonstrates: client.Documents.Preview(ctx, id) — get a stored document's HTML preview.
documents-thumbnails command
Demonstrates: client.Documents.Thumbnails(ctx, id, options) — page thumbnails for a stored document.
Demonstrates: client.Documents.Thumbnails(ctx, id, options) — page thumbnails for a stored document.
poli-page-constructor command
Demonstrates: polipage.NewClient(options) — the only entry point.
Demonstrates: polipage.NewClient(options) — the only entry point.
render-document command
Demonstrates: client.Render.Document(ctx, in) — render and store a PDF server-side.
Demonstrates: client.Render.Document(ctx, in) — render and store a PDF server-side.
render-pdf command
Demonstrates: client.Render.PDF(ctx, in) — renders the project's copy published in the key's environment.
Demonstrates: client.Render.PDF(ctx, in) — renders the project's copy published in the key's environment.
render-pdf-stream command
Demonstrates: client.Render.PDFStream(ctx, in) — renders the project's copy published in the key's environment.
Demonstrates: client.Render.PDFStream(ctx, in) — renders the project's copy published in the key's environment.
render-to-file command
Demonstrates: polipage.RenderToFile(ctx, client, in, path) — streams a PDF to disk.
Demonstrates: polipage.RenderToFile(ctx, client, in, path) — streams a PDF to disk.
internal
clientconfig
Package clientconfig holds the mutable Config used by the polipage client and the option subpackage.
Package clientconfig holds the mutable Config used by the polipage client and the option subpackage.
constants
Package constants holds the SDK-internal string values shared between the transport layer and the namespace implementations: API path templates, canonical HTTP header names, and any other magic strings that would otherwise be re-typed at multiple call sites.
Package constants holds the SDK-internal string values shared between the transport layer and the namespace implementations: API path templates, canonical HTTP header names, and any other magic strings that would otherwise be re-typed at multiple call sites.
http
Package http contains the pure transport-layer helpers used by the top-level polipage client.
Package http contains the pure transport-layer helpers used by the top-level polipage client.
uuid
Package uuid generates RFC 4122 §4.4 version-4 UUIDs from crypto/rand.
Package uuid generates RFC 4122 §4.4 version-4 UUIDs from crypto/rand.
version
Package version exposes the SDK build version, embedded into the User-Agent header by the transport layer.
Package version exposes the SDK build version, embedded into the User-Agent header by the transport layer.
Package option holds the functional options for constructing a polipage.Client and overriding behavior on individual method calls.
Package option holds the functional options for constructing a polipage.Client and overriding behavior on individual method calls.
scripts
extract-api command
Command extract-api walks the public Poli Page Go SDK surface with go/packages + go/doc and emits the docs/src/content/docs/reference/ tree consumed by the Starlight site.
Command extract-api walks the public Poli Page Go SDK surface with go/packages + go/doc and emits the docs/src/content/docs/reference/ tree consumed by the Starlight site.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL