Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/recipient-links-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@simplepdf/embed': patch
---

**Docs: a recipient link opens with `?link=<id>`.** A SimplePDF documents URL carries its recipient link as `?link=<recipient_link_id>`; the editor no longer reads `?prefill=`.
6 changes: 3 additions & 3 deletions copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ See the privacy notes above for the per-route audio-egress disclosure.

### Load a specific document via `?url=`

To open a specific document instead of the bundled demo forms, append `?url=<document-url>`. The value is passed as the `document` to `<EmbedPDF>`; a SimplePDF documents URL (a `/documents/<id>` link, optionally with a `?prefill=<id>`) is navigated to directly by the embed core, so pass a valid one:
To open a specific document instead of the bundled demo forms, append `?url=<document-url>`. The value is passed as the `document` to `<EmbedPDF>`; a SimplePDF documents URL (a `/documents/<id>` link, optionally with a recipient link's `?link=<id>`) is navigated to directly by the embed core, so pass a valid one:

```
http://localhost:3001/?url=https%3A%2F%2Fdemo.simplepdf.com%2Fdocuments%2Fc28f061b-1974-4251-ba7a-d08bedc3ef28%3Fprefill%3D35fdf39e-2e06-4712-bb9d-f62d2f88ce50
http://localhost:3001/?url=https%3A%2F%2Fdemo.simplepdf.com%2Fdocuments%2Fc28f061b-1974-4251-ba7a-d08bedc3ef28%3Flink%3D35fdf39e-2e06-4712-bb9d-f62d2f88ce50
```

URL-encode the value whenever it carries its own query string (e.g. `?prefill=`), otherwise the nested params get parsed as part of the page URL and dropped.
URL-encode the value whenever it carries its own query string (e.g. `?link=`), otherwise the nested params get parsed as part of the page URL and dropped.

The value must be an absolute `http(s)` URL on your configured base-domain family (`*.simplepdf.com` by default, or whatever host `VITE_SIMPLEPDF_BASE_DOMAIN` resolves to). Third-party origins are rejected on purpose: the iframe is granted clipboard access and is wired to the `postMessage` bridge, so framing an arbitrary site would hand it both. A malformed or off-domain `?url=` silently falls back to the default demo form. `?url=` combines with `?lang=`; when set, it wins for what the editor loads.

Expand Down
2 changes: 1 addition & 1 deletion copilot/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const isModelTab = (value: unknown): value is ModelTab =>
// its origin becomes the iframe origin AND the postMessage bridge target — it must
// therefore be a valid document URL on that family (e.g. any `*.simplepdf.com`
// tenant), such as
// https://demo.simplepdf.com/documents/c28f061b-1974-4251-ba7a-d08bedc3ef28?prefill=35fdf39e-2e06-4712-bb9d-f62d2f88ce50
// https://demo.simplepdf.com/documents/c28f061b-1974-4251-ba7a-d08bedc3ef28?link=35fdf39e-2e06-4712-bb9d-f62d2f88ce50
// Rejecting everything else keeps a crafted `?url=javascript:...`, a relative
// path resolving against our own origin, or a third-party origin (which would
// be framed with our clipboard permissions and wired to the bridge) out of the
Expand Down
2 changes: 1 addition & 1 deletion embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ createEmbed({ target, companyIdentifier: 'acme', document: { dataUrl: 'data:appl
createEmbed({ target, companyIdentifier: 'acme', document: { file: pdfFileOrBlob } })
```

- **`url`**: any `http(s)` URL. Fetched from your page first (50 MB cap); on CORS / size / network failure it falls back to the editor's `?open` loader, so CORS-restricted public URLs still load. `user:pass@` credentials are allowed (they route via `?open`). A **SimplePDF documents URL** on your base-domain family (e.g. `https://acme.simplepdf.com/documents/<id>?prefill=<id>`) is navigated to directly, so the editor loads + prefills the stored document itself (your `context` is carried through).
- **`url`**: any `http(s)` URL. Fetched from your page first (50 MB cap); on CORS / size / network failure it falls back to the editor's `?open` loader, so CORS-restricted public URLs still load. `user:pass@` credentials are allowed (they route via `?open`). A **SimplePDF documents URL** on your base-domain family (e.g. `https://acme.simplepdf.com/documents/<document_id>?link=<recipient_link_id>`) is navigated to directly, so the editor loads + prefills the stored document itself (your `context` is carried through).
- **`file`**: a `File` (e.g. from `<input type="file">`) or any `Blob`. Converted for you, no `FileReader`.
- **`dataUrl`**: a `data:` URL string.

Expand Down
4 changes: 2 additions & 2 deletions embed/embed-api.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"editor_version": "7845f7c3-20261014T100758Z",
"editor_version": "6f020890-20261114T210959Z",
"description": "This is the SimplePDF editor interface contract. Drive the editor programmatically over window.postMessage with the editor iframe: post a JSON string { \"type\": {{operation request_type}}, \"request_id\": {{a request correlation id you generate}}, \"data\": {{input matching input_schema}} } to the iframe; the editor replies with { \"type\": \"REQUEST_RESULT\", \"data\": { \"request_id\": {{the same correlation id}}, \"result\": {{result}} } } (match each reply to its request by this id), where result is { \"success\": true, \"data\": {{a value matching the operation's output_schema; null for ops that return nothing}} } or { \"success\": false, \"error\": { code, message } }. Each operation lists the op-specific `error_codes` it can return; on top of those, any op may also fail with a gateway/permission code (origin/plan/signup gating, editor-not-ready, or an internal error). Every code is within `editor_error_schema` (the complete closed union to narrow against), where each code const carries a `description` of its meaning. Outbound events (see `events`) are pushed the same way. `operations` lists every operation the editor supports. Most require the embedding origin to be allowlisted (\"whitelisted\") for the tenant in the SimplePDF admin dashboard, and the tenant plan to permit them (LOAD_DOCUMENT is always available). A call the current setup does not permit returns the matching gateway code: forbidden:origin_not_whitelisted when the origin is not allowlisted, or bad_request:plan_upgrade_required when the plan excludes it. All JSON schemas use the json_schema_dialect declared at the root. We recommend the @simplepdf/embed package for a typed, ergonomic wrapper over this contract (https://github.com/SimplePDF/simplepdf-embed).",
"json_schema_dialect": "https://json-schema.org/draft/2020-12/schema",
"protocol": {
Expand Down Expand Up @@ -1263,7 +1263,7 @@
{
"type": "string",
"const": "bad_request:failed_to_load_document",
"description": "The document LOAD_DOCUMENT was given could not be loaded (unreachable, not a PDF or image, corrupted, password-protected, superseded by another load, or the editor could not prepare it: the server never resolved it, or storing it failed); the message says which."
"description": "The document LOAD_DOCUMENT was given could not be loaded (unreachable, not a PDF or image, corrupted, password-protected, superseded by another load, or the editor could not prepare it: the server never resolved it, storing it failed, or it carries more form fields than a document can); the message says which."
},
{
"type": "string",
Expand Down
4 changes: 2 additions & 2 deletions embed/src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ const assertValidDocument = (document: unknown): void => {
}

// A SimplePDF "documents" URL is a stored document / share route
// (e.g. https://<companyIdentifier>.simplepdf.com/documents/<id>?prefill=<id>),
// (e.g. https://<companyIdentifier>.simplepdf.com/documents/<document_id>?link=<recipient_link_id>),
// not a fetchable PDF. The path is /documents/<id>, optionally locale-prefixed.
const DOCUMENTS_PATH_PATTERN = /^\/(?:[a-z]{2}\/)?documents\/[^/]+\/?$/

Expand Down Expand Up @@ -631,7 +631,7 @@ const mountIntoContainer = (
? ((): string => {
// Navigate straight to the stored document; carry the context through
// (the editor decodes ?context= on the documents route too) while
// preserving the URL's own query, e.g. ?prefill=.
// preserving the URL's own query, e.g. ?link=.
const src = new URL(documentsUrl.url.href)
if (encodedContext !== null) {
src.searchParams.set('context', encodedContext)
Expand Down
4 changes: 2 additions & 2 deletions embed/test/mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe(createEmbed.name, () => {
target: '#root',
companyIdentifier: 'acme',
context: { a: 1 },
document: { url: 'https://demo.simplepdf.com/documents/abc-123?prefill=p-9' },
document: { url: 'https://demo.simplepdf.com/documents/abc-123?link=p-9' },
})
const iframe = document.querySelector('#root iframe')
if (!(iframe instanceof HTMLIFrameElement)) {
Expand All @@ -225,7 +225,7 @@ describe(createEmbed.name, () => {
// Bridges to the URL's own subdomain (demo), not the configured companyIdentifier (acme).
expect(src.origin).toBe('https://demo.simplepdf.com')
expect(src.pathname).toBe('/documents/abc-123')
expect(src.searchParams.get('prefill')).toBe('p-9')
expect(src.searchParams.get('link')).toBe('p-9')
expect(src.searchParams.get('context')).not.toBeNull()
// It is a direct navigation, not the host-fetch /editor path.
expect(src.searchParams.get('loadingPlaceholder')).toBeNull()
Expand Down
6 changes: 3 additions & 3 deletions react/src/embed-pdf.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('EmbedPDF (inline)', () => {
<EmbedPDF
mode="inline"
companyIdentifier="acme"
document={{ url: 'https://demo.simplepdf.com/documents/abc?prefill=p1' }}
document={{ url: 'https://demo.simplepdf.com/documents/abc?link=p1' }}
/>,
);
const iframe = container.querySelector('iframe');
Expand All @@ -103,7 +103,7 @@ describe('EmbedPDF (inline)', () => {
const src = new URL(iframe.getAttribute('src') ?? '');
expect(src.origin).toBe('https://demo.simplepdf.com');
expect(src.pathname).toBe('/documents/abc');
expect(src.searchParams.get('prefill')).toBe('p1');
expect(src.searchParams.get('link')).toBe('p1');
});

it('accepts the deprecated documentURL as an alias for document (deprecated alias)', () => {
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('EmbedPDF (modal, default)', () => {
const user = userEvent.setup();
render(
<EmbedPDF companyIdentifier="acme">
<a href="https://demo.simplepdf.com/documents/abc?prefill=p1">Open</a>
<a href="https://demo.simplepdf.com/documents/abc?link=p1">Open</a>
</EmbedPDF>,
);
await user.click(screen.getByText('Open'));
Expand Down
8 changes: 4 additions & 4 deletions skills/build-with-simplepdf/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,12 +292,12 @@ Canonical flow (relative to the API base in "REST API" below):

1. `GET /documents/{document_id}/fields` — each field has a stable `id`, a `type`, and (for constrained fields) allowed `options`
2. map known values to stable field ids and allowed options
3. `POST /documents/{document_id}/prefills` — returns the prefill `id`, an `upload` object (where and how to send the values), and an `embed_url`
3. `POST /documents/{document_id}/recipient_links` — creates a recipient link (a unique, revocable URL for one person, agent or integration; you deliver it, and anyone holding it can open it) and returns its `id`, an `upload` object (where and how to send the values), and an `embed_url`
4. upload `{ "fields": [{ "id", "value" }] }` per the `upload` object, straight to the customer storage
5. open/share the returned `embed_url`
6. the human reviews, corrects, signs and submits

Rules: keep sensitive values in the prefill blob, not webhook `context`; unknown field ids are ignored and read-only fields ignore prefill values; a field listing `options` only accepts one of them (checkboxes, dropdowns, radios); otherwise the value is a plain string (text fields) or a base64 data URL (signature and picture fields). When mapping from natural-language data, validate constrained values before writing the blob and never let the model invent a field id when the field schema is available. Prefills are deletable (`DELETE /documents/{document_id}/prefills/{prefill_id}`) — clean up prefills that were superseded or never opened.
Rules: keep sensitive values in the prefill blob, not webhook `context`; unknown field ids are ignored and read-only fields ignore prefill values; a field listing `options` only accepts one of them (checkboxes, dropdowns, radios); otherwise the value is a plain string (text fields) or a base64 data URL (signature and picture fields). When mapping from natural-language data, validate constrained values before writing the blob and never let the model invent a field id when the field schema is available. Recipient links are deletable (`DELETE /documents/{document_id}/recipient_links/{recipient_link_id}`) — clean up recipient links that were superseded or never opened.

## Post-submit

Expand All @@ -321,7 +321,7 @@ Use `submission.created` for downstream automation. The delivery carries metadat

## Storage

Only when submissions/prefills/persistence make it relevant, and after checking whether the app already has an established storage boundary (`AskUserQuestion`, header `Storage`):
Only when submissions/recipient links/persistence make it relevant, and after checking whether the app already has an established storage boundary (`AskUserQuestion`, header `Storage`):

**"Where should submitted PDFs and prefill data live?"**

Expand All @@ -336,7 +336,7 @@ Supported BYOS families: S3-compatible object storage, Azure Blob Storage, Share

## REST API

Only when the backend needs lifecycle control beyond browser editor actions/webhooks. Server-side resources: documents, field schemas, prefills, submissions.
Only when the backend needs lifecycle control beyond browser editor actions/webhooks. Server-side resources: documents, field schemas, recipient links, submissions.

```text
https://{companyIdentifier}.simplepdf.com/api/v1
Expand Down
Loading