Frontend Rendering
You can render documents directly from the browser — no proxy endpoint on your backend — using your public key (pk_live_…). The public key is safe to ship to clients because every request must clear three checks:
- Domain allowlist — the request’s
Originhostname must be in your organization’s Allowed domains (exact match or a subdomain of an allowed entry). - Signed user identity — the request must carry
x-user-idandx-user-hash, where the hash is an HMAC-SHA256 of the user ID computed with your signing secret. Only your backend knows the signing secret, so only your backend can mint valid hashes. - Scoping — public-key requests act as that end user, not as your whole organization: they see only that user’s templates, documents, and usage.
The handshake
Step 1 — Allow your domain
In the dashboard, Settings → Allowed domains: add your site’s hostname (e.g. example.com — this also allows app.example.com and other subdomains). Requests from anywhere else are rejected with 401 Domain <hostname> is not in allowedDomains.
Step 2 — Sign the user ID on your backend
Use signUserId from the server entry — it’s a one-liner over Node’s crypto:
import { signUserId } from '@reforgio/sdk-core/server'
// however you resolve the current user…
const userId = 'user-123'
const userHash = signUserId(userId, process.env.REFORGIO_SIGNING_SECRET!)
// return { userId, userHash } to the client with the session responseSign on the server, ship the hash. Never send the signing secret itself to the browser — anyone holding it can impersonate any of your users.
Step 3 — Render from the browser
With the plain TypeScript client:
import { Reforgio, ReforgioError } from '@reforgio/sdk-core'
const client = new Reforgio('pk_live_…', {
userId: 'user-123',
userHash: '<hash from your backend>',
})
try {
const doc = await client.render('YOUR_TEMPLATE_ID', { month: 'June', total: 8200 })
window.open(doc.downloadUrl)
} catch (err) {
if (err instanceof ReforgioError) {
console.error(`Render failed (${err.statusCode}): ${err.message}`)
}
}The browser client also exposes getTemplates() and getTemplate(id) — scoped to the authenticated user.
Step 3, React edition
@reforgio/sdk-react wraps the client in a provider and hooks:
import { ReforgioProvider, useRender } from '@reforgio/sdk-react'
export function App({ userId, userHash, children }: {
userId: string
userHash: string // from your backend — see step 2
children: React.ReactNode
}) {
return (
<ReforgioProvider
publicKey={process.env.NEXT_PUBLIC_REFORGIO_PUBLIC_KEY!}
userId={userId}
userHash={userHash}
>
{children}
</ReforgioProvider>
)
}
export function InvoicePreview({ items }: {
items: Array<{ name: string; quantity: number; price: number }>
}) {
const { result, loading, error, refetch } = useRender('YOUR_TEMPLATE_ID', {
company_name: 'ACME Corp',
items,
})
if (loading) return <p>Generating PDF…</p>
if (error) return (
<div>
<p>Failed to generate: {error.message}</p>
<button onClick={refetch}>Retry</button>
</div>
)
if (!result) return null
return (
<div>
<iframe src={result.downloadUrl} width="100%" height="800px" />
<a href={result.downloadUrl} download>Download PDF</a>
</div>
)
}useRender(templateId, data, format?) renders on mount and re-renders when its inputs change (data is compared by value, so inline object literals are fine). It returns { result, loading, error, refetch }. The package also ships useTemplates(), useTemplate(id), and useReforgio() for direct client access.
What the requests look like
Under the hood every public-key request sends:
| Header | Value |
|---|---|
x-api-key | pk_live_… |
x-user-id | your user’s ID |
x-user-hash | HMAC-SHA256(userId, signingSecret) as hex |
Origin | set automatically by the browser |
Common 401s
| Message | Cause |
|---|---|
Public key requires Origin header | Request didn’t come from a browser context (e.g. server-side fetch with a public key — use the secret key there) |
Domain <hostname> is not in allowedDomains | Origin hostname missing from Settings → Allowed domains |
Public key requires x-user-id and x-user-hash headers | Client constructed without userId/userHash |
Invalid x-user-hash | Hash computed with the wrong secret, for a different user ID, or the keys were rotated |
Full catalog: Errors.