{"revision":"dcd401883172f852","generated_at":"2026-11-10T10:19:18.738Z","site_url":"https://docs.typograph.nl","sections":[{"title":"Portal Documentation","url":"https://docs.typograph.nl/portal/","section":null,"audience":"portal","headings":[],"text":"Welcome to the Typograph Portal user guide.\n\nThe Portal is the web interface where designers and content operators create, edit, organize, and share Typograph templates. It's built around a clear hierarchy:\n\n```\nOrganization → Team → Project → Template / Media / Fonts\n```\n\n- **Organizations** carry the subscription plan and the shared OAuth apps, webhooks, and API quota. You can belong to multiple organizations.\n- **Teams** group collaborators within an organization.\n- **Projects** group related templates and their assets (media, fonts, uploads).\n- **Templates** are the actual Typograph design files, opened in the Editor.\n\nSign-in happens through the separate **Auth UI** service (with Google, Microsoft, or a verification code sent to your email), which then returns you to the Portal. See [Signing In](https://docs.typograph.nl/portal/getting-started/login).","agent_tools":[]},{"title":"Portal Documentation","url":"https://docs.typograph.nl/portal/#portal-sections","section":"Portal Sections","audience":"portal","headings":[],"text":"- **[Getting Started → Signing In](https://docs.typograph.nl/portal/getting-started/login)** — Google, Microsoft, or an email code\n- **[Getting Started → Overview](https://docs.typograph.nl/portal/getting-started/overview)** — key features and where to find them\n- **[Getting Started → Navigation](https://docs.typograph.nl/portal/getting-started/navigation)** — main nav, sidebar, Recents, Favorites, Shared items, Trash, Notifications\n- **[Content → Templates](https://docs.typograph.nl/portal/content/templates)** — the primary content unit\n- **[Content → Assets](https://docs.typograph.nl/portal/content/assets)** — project-scoped media\n- **[Content → Documents](https://docs.typograph.nl/portal/content/documents)** — how \"document\" and \"template\" relate\n- **[Your Workspace](https://docs.typograph.nl/portal/workspace/account)** — your account, search, the assistant, favorites, notifications, and trash\n- **[Users → Roles](https://docs.typograph.nl/portal/users/roles)** — roles and what they can do\n- **[Users → Permissions](https://docs.typograph.nl/portal/users/permissions)** — sharing and resource-level access\n- **[Organization](https://docs.typograph.nl/portal/organization/structure)** — structure, invites, OAuth apps, webhooks, and billing","agent_tools":[]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets","section":null,"audience":"portal","headings":[],"text":"\"Assets\" in Typograph covers two things, both scoped to a **project**:\n\n- **Media** — images and other visual assets used by the project's templates\n- **Fonts** — font files used by the project's templates\n\nBoth live **inside a project** — there is no organization-wide or team-wide \"asset library\". This keeps templates self-contained and portable: everything a template needs is available within the same project.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#navigating-to-assets","section":"Navigating to Assets","audience":"portal","headings":[],"text":"1. Pick an organization, team, and project.\n2. Open the **Media** tab or **Fonts** tab inside the project.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#uploading-files","section":"Uploading Files","audience":"portal","headings":[],"text":"Every tab of a project takes files, and each file goes where it belongs: a PDF, ZIP or JSON becomes a [template](https://docs.typograph.nl/portal/content/templates#importing-a-file-as-a-template), an image goes to **Media**, a font to **Fonts**.\n\n| File | Up to |\n|------|-------|\n| Image | 75 MB (an SVG 25 MB) |\n| Font, ZIP or PDF | 50 MB |\n| JSON | 5 MB |\n\n- **Drag files from your computer anywhere onto the project page.** While you drag, the page says which project the files go to. Let go to upload.\n\n  [Screenshot: A project page while files are dragged over it: the page is dimmed, with the message Drop to upload to Spring campaign, and below it that a PDF, ZIP or JSON becomes a template, images go to Media and fonts to Fonts, and how large each kind of file may be]\n\n- **Or choose them**: click **New template** and choose **Upload files** in the Gallery, or use the upload button on an empty **Media** or **Fonts** tab.\n\nWhile files upload, a panel at the bottom right shows each one with its progress, then **Processing…** while Typograph prepares it. A file that uploaded leaves the panel; one that failed stays with the reason, until you remove it with **×**. When the connection dropped or the server did not answer, the row also has **Retry**: it uploads the same file again. A file that is too large or of a kind Typograph does not take has no **Retry**: the reason says what to change. Only people who can edit the project can upload to it.\n\n[Screenshot: The Uploads panel at the bottom right of the page, with the file spring-poster-photo.png of 0.2 MB being processed]","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#uploading-media","section":"Uploading Media","audience":"portal","headings":["What Typograph Keeps of an Image","Supported Image Formats"],"text":"1. Open the project's **Media** tab.\n2. Drag images onto the page, or click **Upload media** on an empty tab and select them. See [Uploading Files](https://docs.typograph.nl/portal/content/assets#uploading-files).\n\n### What Typograph Keeps of an Image\n\nAn uploaded image is kept exactly as you uploaded it, the **original**. Next to it, Typograph makes the versions the platform works with:\n\n- a **print** version, the one your designs are printed and exported with\n- a **screen** version and a **thumbnail**, sized for the Editor and the Portal and sharp on high-resolution screens\n\nAn image counts toward your organization's storage at its largest version: the original or the print version, whichever is larger, which for a photo is usually the original. The screen version and the thumbnail do not count: see [Subscription & Usage](https://docs.typograph.nl/portal/organization/billing#checking-your-usage).\n\n### Supported Image Formats\n\nJPEG (`jpg`, `jpeg`), PNG, GIF, SVG, WebP, TIFF (`tif`, `tiff`), BMP, Photoshop (`psd`), Illustrator (`ai`) and EPS. The extension may be in upper or lower case: a camera's `PHOTO.JPG` uploads like `photo.jpg`. Large Photoshop documents (`psb`) are not supported; save them as `psd` or `tif` first.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#uploading-fonts","section":"Uploading Fonts","audience":"portal","headings":["Supported Font Formats"],"text":"1. Open the project's **Fonts** tab.\n2. Drag a font file onto the page, or click **Upload font** on an empty tab. See [Uploading Files](https://docs.typograph.nl/portal/content/assets#uploading-files).\n3. Fonts are made available to all templates inside the project.\n\nEach font in the list shows its size next to its name and designer: what it takes of your organization's storage.\n\n### Supported Font Formats\n\nTTF, OTF, WOFF, WOFF2 (and EOT on input-only via the Converter). TTF/OTF are typical for design workflows; WOFF2 is the most efficient for web delivery.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#managing-uploaded-files","section":"Managing Uploaded Files","audience":"portal","headings":["Moving a File to the Trash"],"text":"Both Media and Fonts support the same actions from their list view:\n\n| Action | What it does |\n|--------|--------------|\n| **Tags** *(media only)* | Adds and removes the file's tags, see [Tagging Media](https://docs.typograph.nl/portal/content/assets#tagging-media) |\n| **Download** | Downloads the file as it was uploaded |\n| **Rename** | Changes the file's display name |\n| **Move** | Moves the file to a different project — pick the destination project. It then belongs to that project and leaves the original one's list |\n| **Move to trash** | Moves the file to the [Trash](https://docs.typograph.nl/portal/workspace/trash), after you confirm. Anyone who can edit the project can restore it from there for 30 days; after that it is deleted for good |\n\n### Moving a File to the Trash\n\n1. Open the file's **⋮** menu and choose **Move to trash**.\n\n   [Screenshot: The menu of the media file Portrait Mark, with Tags, Download, Rename, Move and Move to trash]\n\n2. Confirm with **Move to trash**. **Cancel** leaves the file where it is.\n\n   [Screenshot: The Move media to trash? dialog, saying the media file will move to the trash and that anyone who can edit the project can still restore it from there, with the buttons Move to trash and Cancel]\n\n3. The file leaves the list, and a message says **Media moved to trash** (for a font: **Font moved to trash**).\n\n   [Screenshot: The message Media moved to trash]\n\nTo get a file back, open [Trash](https://docs.typograph.nl/portal/workspace/trash#restoring-an-item) and restore it: it returns to the project's Media or Fonts tab.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#viewing-and-downloading-media","section":"Viewing and Downloading Media","audience":"portal","headings":[],"text":"Click a media card to see the image larger. It opens fitted to the window:\n\n[Screenshot: The enlarged media file Portrait Lisa, with WEBP, 960 × 1440 px, 250.5KB and Uploaded Nov 1 under its name, fitted to the window at 44%, with the zoom out and zoom in buttons around the zoom level at the bottom left, and Download and Close at the bottom right]\n\n- **Under its name** you see what kind of file it is (such as JPEG or PNG), its size in pixels for an image (such as 1200 × 700 px), how large the uploaded file is, and the day it was uploaded. A drawing (SVG, AI, EPS or PDF) has no size in pixels, as it stays sharp at any size.\n- **Zoom** with the **+** and **−** buttons or keys, with Ctrl (Cmd on a Mac) and the scroll wheel, or by pinching on a trackpad. Double-click to zoom in on a spot, and again to fit the image back. The button between them shows how far you are zoomed in; click it, or press **0**, to fit the image to the window again.\n- **Move around** a zoomed image by dragging it, or with the scroll bars.\n- **Download** saves the file as it was uploaded, under its name in the portal.\n\n**Download** is also in each media file's **⋮** menu. Media uploaded before Typograph kept originals downloads as the file the editor prints with.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#finding-media","section":"Finding Media","audience":"portal","headings":["Tagging Media"],"text":"Searching, sorting and tags sit behind **Search and filter**, at the right of the tabs, so the media grid starts right below them. Click it to open the panel, click it again to close it. While a filter is on, the button shows how many filters are on, also with the panel closed.\n\n[Screenshot: The tabs Templates, Media, Fonts, Clips and Forms with Media chosen, the button Search and filter at the right, and below it the media grid with Spring illustration, Star shape and Flower shape]\n\nIn the panel:\n\n- **Search by name** keeps the media whose name contains what you type.\n- **Sort** puts them in order: newest first (the default), oldest first, by name, or largest first.\n- **Tags**: once the project's media have tags, each tag is a button with the number of media it is on. Click one to show only those media, click it again to drop it. **Without a tag**, the first button, shows the media nobody has tagged yet; choosing it drops the chosen tags, and choosing a tag drops it. A project with many tags shows its most used ones and the ones you chose; **More tags** shows them all.\n\n[Screenshot: Search and filter opened, its count at 2: Search by name and Newest first, then Without a tag and the tags Shapes and Spring campaign, both chosen. Below them 3 media files, Any selected tag chosen beside All selected tags, and Clear filters. The grid shows Spring illustration, Star shape and Flower shape with their tags]\n\nOnce a filter is on, the panel says how many media files it found. With two tags or more chosen, pick what you are after: **All selected tags** shows media with every tag (the default), **Any selected tag** media with at least one. **Clear filters** shows all media again. On a phone the tags sit in one row you swipe sideways. The filter is in the page's address, so a link to it opens the same selection, with the panel open.\n\nClose the panel while a filter is on and one line above the grid keeps saying what you see: how many media files, which filters, and **Clear filters**. A file you upload that does not match the filter does not show until you clear it.\n\n[Screenshot: Search and filter closed, its count at 2. Above the grid: 3 media files, Filtered by Shapes, Spring campaign, and Clear filters. The grid shows Spring illustration, Star shape and Flower shape]\n\n### Tagging Media\n\nTags are words you give media to find them again: a client, a campaign, *Logos*, *Print only*. Each project has its own tags, and a tag you made once is there to reuse on every media file of the project.\n\n1. Open the media file's **⋮** menu and choose **Tags**.\n   [Screenshot: The Media tags dialog of Portrait Mark, with its tags People and Spring campaign, each with an x to remove it, the field Add a tag with the button Add, and Done]\n\n2. Type a tag and click **Add**. While you type, the project's tags are suggested: pick one to reuse it, or type a new one. Capitals and spaces do not make a different tag: *logos* is the same tag as *Logos*.\n3. To take a tag off, click the **×** next to it.\n4. Click **Done**. The card shows its first two tags, and how many more it has.\n\nAnyone who can edit the project can tag its media; everyone who can see the media sees its tags. A media file holds up to 20 tags. A file you move to another project takes its tags along, and a file in the trash keeps them.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#using-assets-in-a-template","section":"Using Assets in a Template","audience":"portal","headings":[],"text":"Inside the Editor:\n\n- **Media** — drag an image from the media panel onto the canvas, or insert an Image element and pick from the project's media.\n- **Fonts** — available in the font picker on any rich-text element.\n\nAssets stay scoped to their project. To reuse an image across projects, upload it into each, or move templates that depend on it into the same project.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Managing Assets","url":"https://docs.typograph.nl/portal/content/assets#api-equivalents","section":"API Equivalents","audience":"portal","headings":[],"text":"| Portal action | API endpoint |\n|---------------|--------------|\n| List media | `GET /v1/file/projects/{projectId}/media` |\n| Show media (read links) | `POST /v1/file/projects/{projectId}/media/presigned-urls` |\n| List fonts | `GET /v1/file/projects/{projectId}/fonts` |\n| Load fonts (read links) | `POST /v1/file/projects/{projectId}/fonts/presigned-urls` |\n| Find media by tag or name, sorted | `GET /v1/file/projects/{projectId}/media` with `tag_ids`, `tag_match`, `untagged`, `search` and `sort` |\n| Tag media | `POST /v1/file/projects/{projectId}/media/tags` |\n| List the project's tags | `GET /v1/file/projects/{projectId}/tags` |\n| Move media to the trash, or restore it | `DELETE /v1/file/projects/{projectId}/media/{id}` (`?action=undelete` restores) |\n| Move a font to the trash, or restore it | `DELETE /v1/file/projects/{projectId}/fonts/{id}` (`?action=undelete` restores) |\n| Upload media or fonts | `POST /v1/file/projects/{projectId}/uploads`, `PUT` the file to the link it returns, then `PATCH /v1/file/projects/{projectId}/uploads/{id}` to finalize |\n\nSee [File Service](https://docs.typograph.nl/guides/api/file-service) for the full list.","agent_tools":["media_list","media_get_urls","media_tag","tag_list","tag_rename","tag_delete","media_delete","font_list","font_update","font_get_urls","font_delete","upload_create","upload_process"]},{"title":"Documents vs Templates","url":"https://docs.typograph.nl/portal/content/documents","section":null,"audience":"portal","headings":[],"text":"A note on terminology before you dig in: Typograph's primary content unit is the **template**. The word \"document\" is used more loosely, and the two relate like this:\n\n- A **template** is the versioned design file you create in the Editor, store in a project, and render with the Publisher. Templates have UUIDs, permissions, comments, invites — all the collaborative machinery.\n- A **document** is the **output** of running a template through the Publisher (a PDF, image, or set of images), or the JSON payload the API validates via the Document Service.\n\nThere is no separate \"documents\" section in the Portal. If you want to:\n\n- **Create new design content** → create a **[template](https://docs.typograph.nl/portal/content/templates)** inside a project.\n- **Generate a PDF or image from a template** → that's a publisher job, triggered from the template's menu or via the API.\n- **Upload images or fonts** → those go under the project as **[media / assets](https://docs.typograph.nl/portal/content/assets)**.","agent_tools":[]},{"title":"Documents vs Templates","url":"https://docs.typograph.nl/portal/content/documents#why-this-matters","section":"Why This Matters","audience":"portal","headings":[],"text":"If you're used to tools where \"documents\" and \"templates\" are separate things (one is the reusable structure, the other is the filled-in instance), Typograph collapses that distinction: you always work with templates. When you need a one-off variant, duplicate the template. When you need a batch, one output per row of a CSV, run a publisher job through the API: each column of the CSV fills a merge field of the template, a named text frame or image, whether an element prints, or a `{{key}}` placeholder ([what the CSV holds](https://docs.typograph.nl/guides/api/publisher#what-the-csv-holds)).","agent_tools":[]},{"title":"Documents vs Templates","url":"https://docs.typograph.nl/portal/content/documents#actions-on-a-template","section":"Actions on a Template","audience":"portal","headings":[],"text":"From the template list or the Editor, you can:\n\n| Action | What it does |\n|--------|--------------|\n| **Edit** | Open in Editor |\n| **Duplicate** | Create a copy inside the same project |\n| **Move** | Move to another project (or team) |\n| **Invite** | Share with a specific person and role |\n| **Favorite** (heart icon) | Add to Favorites for quick access |\n| **Comment** | Attach a comment to the template |\n| **Export** / **Publish** | Trigger a publisher job (PDF / image) |\n| **Move template to trash** | Moves it to the Trash, from the Editor's **⋮** menu; it can be restored from there |\n| **Restore** | Available from the [Trash](https://docs.typograph.nl/portal/workspace/trash) in the sidebar |","agent_tools":[]},{"title":"Documents vs Templates","url":"https://docs.typograph.nl/portal/content/documents#api-equivalents","section":"API Equivalents","audience":"portal","headings":[],"text":"| Portal action | API endpoint |\n|---------------|--------------|\n| Create template | `POST /v1/file/templates` or `POST /v1/file/projects/{id}/templates` |\n| Edit (autosave) | `PATCH /v1/file/templates/{id}` |\n| Render to PDF | `POST /v1/publisher/jobs` |\n| Validate schema | `POST /v1/document/validate` |\n\nSee [File Service](https://docs.typograph.nl/guides/api/file-service) and [Publisher](https://docs.typograph.nl/guides/api/publisher) in the developer docs for full details.","agent_tools":[]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates","section":null,"audience":"portal","headings":[],"text":"Templates are the primary content unit in Typograph. This page covers the lifecycle you'll use in the Portal. For the document JSON schema see the [developer concepts](https://docs.typograph.nl/guides/concepts/documents).","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#where-templates-live","section":"Where Templates Live","audience":"portal","headings":[],"text":"Every template belongs to a **project**, which belongs to a **team**, which belongs to an **organization**. To find or create one:\n\n1. Pick an organization (top-left picker).\n2. Pick a team in the sidebar.\n3. Pick (or create) a project.\n4. Open the **Templates** tab inside the project.","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#creating-a-template","section":"Creating a Template","audience":"portal","headings":["Starting From the Gallery","In an Empty Project","Importing a File as a Template"],"text":"Every way to start a template is in the project's **Gallery**: a blank template, your own files, or a ready-made design.\n\n1. Open a project.\n2. Click **New template** at the top right, or the **New template** tile at the end of the template list. The **Gallery** opens.\n\n   [Screenshot: The header of the project Demo project, with the New template button at the top right next to the project's members, Share and the project menu]\n\n3. Choose how to start:\n   - **Blank template** opens the Editor on a blank canvas. Your work is saved as you go.\n   - **Upload files** opens a file picker. The Gallery closes and a panel at the bottom right shows the upload's progress. See [Importing a File as a Template](https://docs.typograph.nl/portal/content/templates#importing-a-file-as-a-template).\n   - A design copies a ready-made template into this project, as described below.\n\n   [Screenshot: The Gallery dialog of a project, with the categories All, Brochures, Catalogs and Menus, the tiles Blank template and Upload files, and designs such as Product Catalog, Brasserie Menu, Pizzeria Menu and Trifold Brochure]\n\n### Starting From the Gallery\n\nThe gallery holds ready-made designs to start from, such as menus, brochures and catalogs.\n\n1. Click **New template**.\n2. Pick a category at the top to see only those designs, or stay on **All**. Only categories that hold at least one design are shown.\n3. Click a design to see it large, with its number of pages, its size and a short description. **Back to the gallery** returns to the list.\n4. Click **Use template**. The design is copied into this project and opens in the Editor.\n\n   [Screenshot: The design Product Catalog in the gallery, shown large, with 24 pages and A4 Portrait below its title, its description, and the buttons Back to the gallery and Use template]\n\nA design that says **This template cannot be used yet.** can be looked at but not copied.\n\nThe gallery is also on **Recents**. Using a design from there asks which team and project it goes into.\n\n### In an Empty Project\n\nA project without templates shows **No templates in this project yet** with a **New template** button, which opens the Gallery. You can also drag a PDF, ZIP or JSON file anywhere on the page to import it as a template.\n\n[Screenshot: The Templates tab of the empty project Summer campaign: No templates in this project yet, the New template button, and the line Or drag a PDF, ZIP or JSON anywhere on this page]\n\n### Importing a File as a Template\n\nA PDF, a Typograph ZIP package or a Typograph JSON file becomes a template when you upload it: drag it onto the project page, or click **New template** and choose **Upload files** in the Gallery. See [Uploading Files](https://docs.typograph.nl/portal/content/assets#uploading-files).\n\nTo keep a copy outside the Portal, use **File → Save as…** inside the Editor: it downloads the template to your computer as a Typograph file. See [Saving and Exporting](https://docs.typograph.nl/portal/editor/saving-and-exporting).","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#editing","section":"Editing","audience":"portal","headings":[],"text":"Click a template to open the Editor. Changes autosave as you work. Use the file panel on the left to navigate pages, elements, fonts, and swatches. See the [Editor SDK reference](https://docs.typograph.nl/editor/latest) for detail on the editor UI (the Portal embeds the same SDK).","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#favoriting","section":"Favoriting","audience":"portal","headings":[],"text":"Click the heart icon on any template card (or in the Editor header) to add it to your **Favorites**. Favorited items appear in the sidebar for quick access.","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#sharing-a-template","section":"Sharing a Template","audience":"portal","headings":[],"text":"Templates can be shared with people outside the team using **Invite**:\n\n1. Open the template's menu and choose **Invite**.\n2. Enter the collaborator's email and pick a role (see [Roles](https://docs.typograph.nl/portal/users/roles)).\n3. They receive an invite — the link also appears in their **Notifications** and **Shared with me** once accepted.\n\nShared templates show up under **Shared with me → Shared Templates** in the sidebar.","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#comments","section":"Comments","audience":"portal","headings":[],"text":"Use comments to leave feedback on specific places in a template:\n\n- Press the **Comments** button (the pin) in the bar at the top of the Editor. You switch from\n  editing to commenting, and the comments open beside the page.\n- Click the place on the page the comment is about, and type it.\n- Replies thread under the parent comment.\n- Resolve a comment when the feedback is addressed.\n- Press **Comments** again, or close the comments, to go back to editing.\n\nIf you can only comment on or view a template, the Editor opens in that mode and the bar says so;\nthe **Comments** button then only opens and closes the comments.","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#publishing-export-to-pdf--image","section":"Publishing (Export to PDF / Image)","audience":"portal","headings":[],"text":"To render a template into a PDF or image:\n\n1. Open the template.\n2. Choose **Export** → pick PDF / PNG / JPEG / WebP.\n\nA batch with one output per row of a CSV runs through the API, not the Portal: see [Batch Processing with CSV](https://docs.typograph.nl/guides/api/publisher#batch-processing-with-csv).","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#renaming-moving-and-deleting-from-the-project","section":"Renaming, Moving and Deleting from the Project","audience":"portal","headings":[],"text":"Each template's card in a project has a **⋮** menu, next to the heart, so you can tidy up without opening the Editor:\n\n- **Rename** changes the template's name.\n- **Move** takes it to another project: pick the team, then the project, and confirm.\n- **Move to trash**, for the project's admins, moves it to the trash after you confirm. You stay in the project, with the message **Template moved to trash**.\n\n[Screenshot: The card of the template Spring poster A3, with its menu open: Rename, Move and Move to trash]","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Managing Templates","url":"https://docs.typograph.nl/portal/content/templates#deleting-and-restoring","section":"Deleting and Restoring","audience":"portal","headings":[],"text":"A template you no longer need goes to the trash first, where it can be restored. You can do this from its card in the project (above), or from the Editor:\n\n1. Open the template in the Editor, open the **⋮** menu at the top right, and choose **Move template to trash**.\n\n   [Screenshot: The Editor's template menu, with Rename, Move and Move template to trash]\n\n2. Confirm with **Move to trash**.\n\n   [Screenshot: The Move template to trash? dialog, saying the template will move to the trash and that the template's admins can still restore it from there, with the buttons Move to trash and Cancel]\n\n3. You are taken to the team's projects, with the message **Template moved to trash**.\n\nFrom [Trash](https://docs.typograph.nl/portal/workspace/trash#restoring-an-item) the template's admins can restore it, as the admins of a project or team can restore that. After the retention period expires, trashed items are permanently removed.","agent_tools":["template_list","template_get","template_create","template_update","template_delete","template_restore","premade_template_list","premade_template_get","premade_template_use","upload_create","upload_process","publisher_job_create"]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview","section":null,"audience":"portal","headings":[],"text":"The Editor opens whenever you create or click a template in the Portal. It's where you design — adding text, shapes, and images to your pages. This page gives you a tour of the layout before the other Editor guides walk through specific tools.","agent_tools":[]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview#opening-the-editor","section":"Opening the Editor","audience":"portal","headings":[],"text":"1. In the Portal, open a project and go to its **Templates** tab.\n2. Click an existing template card. To start a new one, click **New template** and choose **Blank template** in the Gallery (see [Creating a Template](https://docs.typograph.nl/portal/content/templates#creating-a-template)).\n\n   [Screenshot: The project's Templates tab, with a template card and the New template tile]\n   *Click a template card here to open it in the Editor.*\n\n3. The Editor opens with your template loaded, ready to edit. Changes autosave as you work — there's no separate \"save\" step for normal edits.\n\n   [Screenshot: The Editor freshly opened with a template loaded]","agent_tools":[]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview#finding-your-way-around","section":"Finding Your Way Around","audience":"portal","headings":[],"text":"The Editor screen is split into five areas:\n\n| Area | Location | What it's for |\n|---|---|---|\n| **Menu bar** | Top | File, Edit, Select, View, Page, Arrange, Insert, and Help menus |\n| **Toolbar** | Below the menu bar | Quick access to the tools you'll use most (select, text, shapes, zoom) |\n| **Left panel** | Left side | Switch between **Pages**, **Text** snippets, **Shapes**, **Images**, **Clips**, and **Layers** |\n| **Canvas** | Center | Your page — this is what you're designing |\n| **Right panel** | Right side | Properties for whatever is selected — the page itself, or an element on it |\n\n[Screenshot: The five areas of the Editor screen, labeled above]\n*Top to bottom, left to right: menu bar, toolbar, left panel (currently on the Pages tab), canvas, right panel.*\n\n1. Look at the left panel and click through its tabs: **Pages**, **Text**, **Shapes**, **Images**, **Clips**, **Layers**. Each tab shows a different set of things you can drag or click onto the canvas.\n2. Click an empty area of the page (nothing selected). The right panel shows **page properties** — name, size, background color, bleed.\n\n   [Screenshot: The right panel showing page properties when nothing is selected]\n\n3. Click any element already on the page. The right panel switches to **element properties** — position, size, rotation, and other options specific to that element.\n\n   [Screenshot: The right panel switched to element properties after selecting a shape]\n\n4. Click away from the element (on empty canvas) to deselect it and return to page properties.","agent_tools":[]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview#zooming-and-viewing-your-page","section":"Zooming and Viewing Your Page","audience":"portal","headings":[],"text":"1. Open the **View** menu.\n2. Choose **Zoom In** or **Zoom Out** to change your zoom level, or **Actual Size** to reset it to 100%.\n3. Choose **Fit Page in Window** to zoom so the whole page fits on screen — the fastest way to get your bearings after zooming in to work on detail.\n4. Still in the **View** menu, turn the rulers and guides on or off. Each item names what it will do: it reads **Show Rulers** while the rulers are hidden and **Hide Rulers** while they are shown, and the same goes for **Show Guides** and **Hide Guides**.\n\n   [Screenshot: The View menu open, showing zoom and ruler/guide options]","agent_tools":[]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview#when-a-template-is-full","section":"When a Template Is Full","audience":"portal","headings":[],"text":"At most 8 people can work in one template at the same time. Everyone who has the template open counts, whatever they may do in it; a second window of your own does not count again.\n\nWhen you open a template that 8 other people already have open:\n\n1. The template opens read-only. The top bar says **8 people are editing**; point at it to read why: you can edit as soon as one of them leaves.\n2. Leave the window open. The Editor asks again for a place by itself, every 10 seconds or so.\n3. As soon as someone closes the template, you take their place: the notice goes away, **You can edit now.** appears, and you edit as usual.\n\nIf you may only view the template, the top bar says **This template is full** instead, and the live view of the others' pointers starts once a place frees.","agent_tools":[]},{"title":"Editor Overview","url":"https://docs.typograph.nl/portal/editor/overview#next-steps","section":"Next Steps","audience":"portal","headings":[],"text":"Now that you know your way around, continue with:\n\n- [Adding and Styling Text](https://docs.typograph.nl/portal/editor/text)\n- [Adding Shapes and Images](https://docs.typograph.nl/portal/editor/shapes-and-images)\n- [Managing Pages and Layers](https://docs.typograph.nl/portal/editor/pages-and-layers)\n- [Saving, Undo/Redo, and Exporting](https://docs.typograph.nl/portal/editor/saving-and-exporting)","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers","section":null,"audience":"portal","headings":[],"text":"Most templates have more than one page, and most pages have more than one overlapping element. This page covers organizing both.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#adding-a-page","section":"Adding a Page","audience":"portal","headings":[],"text":"1. Open the **Pages** tab in the left panel to see all the pages in your template.\n\n   [Screenshot: The Pages tab in the left panel, showing the page thumbnail strip]\n\n2. Open the **Page** menu and choose **New Page** — it's added after your current page.\n\n   [Screenshot: The Page menu open, with New Page, Duplicate Page, and Delete Page]\n\n3. Click the new page's thumbnail in the left panel to switch to it.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#duplicating-and-deleting-a-page","section":"Duplicating and Deleting a Page","audience":"portal","headings":[],"text":"1. Select the page you want to copy in the **Pages** tab.\n2. Open the **Page** menu and choose **Duplicate Page** — an identical copy is added right after it.\n3. To remove a page, select it and choose **Delete Page** from the **Page** menu. If you have multiple pages selected, this becomes **Delete Pages**.\n\nBoth items are in the **Page** menu shown under Adding a Page.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#changing-page-settings","section":"Changing Page Settings","audience":"portal","headings":[],"text":"1. Click an empty area of the page (make sure nothing is selected).\n2. The right panel shows the page's properties: **name**, **width and height**, **type**, **background color**, and **bleed**.\n\n   [Screenshot: The right panel showing page properties: name and size, and bleed]\n\n3. Update the **name** field so the page is easy to identify in the Pages tab.\n4. Adjust **width and height** to resize the page itself (not an element on it).\n5. Turn on **bleed** and set the top/right/bottom/left values if your template needs printer's bleed — use the link button next to the bleed values to set all four sides at once.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#layering-elements","section":"Layering Elements","audience":"portal","headings":[],"text":"When elements overlap, their stacking order determines what's on top.\n\n1. Select an element that's hidden behind another one.\n2. Open the **Arrange** menu and choose **Bring to Front** to move it above everything else, or **Bring Forward** to move it up just one level.\n3. Use **Send to Back** or **Send Backward** the same way to move an element down the stack.\n\n   [Screenshot: The Arrange menu open, with Bring to Front, Bring Forward, Send to Back, and Send Backward visible]","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#using-the-layers-panel","section":"Using the Layers Panel","audience":"portal","headings":[],"text":"1. Open the **Layers** tab in the left panel to see every element on the current page, listed in stacking order.\n\n   [Screenshot: The Layers tab in the left panel, listing the page's elements in stacking order]\n\n2. Click a layer's name to select that element on the canvas — useful when elements overlap too much to click directly.\n3. Drag a layer up or down in the list to reorder it, same effect as the Arrange menu commands above.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#grouping-elements","section":"Grouping Elements","audience":"portal","headings":[],"text":"1. Select multiple elements at once (click one, then shift-click the others, or drag a selection box around them).\n2. Open the **Arrange** menu and choose **Group** — they now move, resize, and select together as one unit.\n\n   [Screenshot: The Arrange menu open, with Group and Ungroup visible]\n\n3. To edit one element inside the group again, select the group and choose **Ungroup** from the same menu.","agent_tools":[]},{"title":"Managing Pages and Layers","url":"https://docs.typograph.nl/portal/editor/pages-and-layers#next-steps","section":"Next Steps","audience":"portal","headings":[],"text":"Continue with [Saving, Undo/Redo, and Exporting](https://docs.typograph.nl/portal/editor/saving-and-exporting).","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting","section":null,"audience":"portal","headings":[],"text":"The last things to know before you're comfortable in the Editor day-to-day: how your work is saved, how to undo mistakes, and how to get your finished design out as a file.","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting#autosave","section":"Autosave","audience":"portal","headings":[],"text":"1. Just keep working — there's nothing to click. The Editor saves your changes automatically as you go.\n2. If you want to check your work is current, open the **File** menu — **Save** is available if you ever want to trigger a save manually, but it's not required for normal editing.\n\n   [Screenshot: The File menu open, with Save visible]","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting#undo-and-redo","section":"Undo and Redo","audience":"portal","headings":[],"text":"1. Made a mistake? Open the **Edit** menu and choose **Undo**, or use the keyboard shortcut shown next to it.\n2. Changed your mind about the undo? Choose **Redo** from the same menu to reapply it.\n\n   [Screenshot: The Edit menu open, with Undo and Redo visible]\n\n3. Undo and Redo step through your recent changes one at a time, so you can safely experiment and step back if something doesn't work out.","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting#saving-a-copy","section":"Saving a Copy","audience":"portal","headings":[],"text":"Sometimes you want to keep your current work as a starting point for something else, rather than continuing to edit the same template:\n\n1. Open the **File** menu and choose **Save as…** to download the current document to your computer as a Typograph file.\n2. Choose **Save as Clip** to save a selection of elements (a logo block, a footer, a repeated design piece) so you can reuse just that piece in other templates via the **Clips** tab.\n\nTo start a new template in the Portal instead, click **New template** on the project's **Templates** tab and choose how to start in the Gallery (see [Creating a Template](https://docs.typograph.nl/portal/content/templates#creating-a-template)).","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting#exporting-your-design","section":"Exporting Your Design","audience":"portal","headings":[],"text":"When your template is ready to share or print, export it as a PDF or image rather than sending the editable template itself. This is the same **Export** feature described in the Portal guide's [Managing Templates](https://docs.typograph.nl/portal/content/templates) page — open that page's **Publishing (Export to PDF / Image)** section for the full steps.","agent_tools":[]},{"title":"Saving, Undo/Redo, and Exporting","url":"https://docs.typograph.nl/portal/editor/saving-and-exporting#youre-all-set","section":"You're All Set","audience":"portal","headings":[],"text":"That covers the Editor's core tools. For everything about the Portal around the Editor — projects, sharing, comments, trash — see the [Portal Overview](https://docs.typograph.nl/portal/getting-started/overview).","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images","section":null,"audience":"portal","headings":[],"text":"Shapes and images give your template visual structure — backgrounds, dividers, photos, and icons. This page covers adding and styling both.","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#adding-a-shape","section":"Adding a Shape","audience":"portal","headings":[],"text":"1. Open the **Insert** menu and choose **Create Rectangle** or **Create Rounded Rectangle**.\n\n   [Screenshot: The Insert menu open, with Create Rectangle and Create Rounded Rectangle visible]\n\n2. Click and drag on the canvas to draw the shape at the size you want.\n3. The shape appears, selected and ready to style.\n\nYou can also open the **Shapes** tab in the left panel to browse a wider set of ready-made shapes and click one to add it to the page.\n\n[Screenshot: The Shapes tab in the left panel, showing ready-made shapes to add]","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#styling-a-shape","section":"Styling a Shape","audience":"portal","headings":[],"text":"1. With the shape selected, use the fill color swatch in the toolbar above the canvas to set its **fill color**.\n2. If it's a rounded rectangle, adjust the **Corners** fields in the right panel to make each corner more or less rounded.\n3. In the **Shadow** section of the right panel, set the shadow's **color**, **length**, **blur**, and **angle**.\n\n   [Screenshot: The right panel for a selected rounded rectangle, with its position and size, Corners, and Shadow sections]","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#adding-an-image","section":"Adding an Image","audience":"portal","headings":[],"text":"1. Open the **Images** tab in the left panel.\n\n   [Screenshot: The Images tab in the left panel]\n\n2. Upload a new image, or click one already available in your project's media library.\n3. Click the image to add it to the current page — it appears in the center of the canvas.","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#positioning-and-resizing-an-image","section":"Positioning and Resizing an Image","audience":"portal","headings":[],"text":"1. Click the image once to select it.\n2. Drag it to reposition, or drag a corner handle to resize it — hold the shape's aspect ratio by dragging from a corner rather than an edge.\n3. For precise placement, use the **X/Y position** and **width/height** fields in the right panel instead of dragging.\n\n*(Positioning fields work the same way as for shapes and text boxes — see the position/size panel in [Adding and Styling Text](https://docs.typograph.nl/portal/editor/text) for what that looks like.)*","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#wrapping-text-around-a-shape-or-image","section":"Wrapping Text Around a Shape or Image","audience":"portal","headings":[],"text":"If you have a text box overlapping a shape or image and want the text to flow around it instead of behind or in front of it:\n\n1. Select the shape or image the text should wrap around.\n2. In the right panel, open the **Text wrap** section.\n3. Choose one of the four wrap modes: **no wrap**, around the **bounding box**, **jump** past the object to the next available space, or following the object's **shape**.\n4. Set the **standoff** for each side (top, left, bottom, right) to control how much space is left between the text and the object's edge.\n\n   [Screenshot: The Text wrap section in the right panel, with its four wrap modes and the bounding box mode selected]","agent_tools":[]},{"title":"Adding Shapes and Images","url":"https://docs.typograph.nl/portal/editor/shapes-and-images#next-steps","section":"Next Steps","audience":"portal","headings":[],"text":"Continue with [Managing Pages and Layers](https://docs.typograph.nl/portal/editor/pages-and-layers).","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text","section":null,"audience":"portal","headings":[],"text":"Text boxes are one of the most common elements you'll add to a template. This page covers adding text, styling it, and reusing text snippets. See the [Editor Overview](https://docs.typograph.nl/portal/editor/overview) if you haven't already toured the layout.","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#adding-a-text-box","section":"Adding a Text Box","audience":"portal","headings":[],"text":"1. Open the **Insert** menu and choose **Create Text Box**.\n\n   [Screenshot: The Insert menu open, with Create Text Box visible]\n\n2. Click and drag on the canvas to draw the text box at the size you want.\n3. The text box appears with a blinking cursor, ready for typing.\n4. Type your text.\n5. Click outside the text box to finish editing and deselect it.","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#styling-your-text","section":"Styling Your Text","audience":"portal","headings":[],"text":"1. Double-click the text box to enter edit mode, then select the text you want to style (drag across it, or use **Select → All Text** from the menu to select every text element on the page at once).\n2. Use the text formatting toolbar above the canvas to change the **font**, **style**, **size**, and **alignment**.\n\n   [Screenshot: The text formatting toolbar with font, size, and alignment controls, shown with text selected]\n\n3. Click outside the text box to see your changes applied.","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#positioning-and-resizing-a-text-box","section":"Positioning and Resizing a Text Box","audience":"portal","headings":[],"text":"1. Click the text box once (without double-clicking) to select it as an element, rather than editing its content.\n2. In the right panel's position and size section, adjust the **X/Y position** to move it, or the **width and height** fields to resize it precisely.\n\n   [Screenshot: The right panel's position and size fields for a selected text box]\n\n3. Alternatively, drag the box directly on the canvas to move it, or drag one of its corner handles to resize it.","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#filling-in-placeholder-text","section":"Filling in Placeholder Text","audience":"portal","headings":[],"text":"If you want to see how a layout looks with a full paragraph before writing real copy:\n\n1. Select an empty text box.\n2. Open the **Insert** menu and choose **Fill with Placeholder Text**.\n\n   [Screenshot: The Insert menu open, with Fill with Placeholder Text visible]\n\n3. The box fills with dummy text so you can check spacing and sizing, then replace it with your real content when you're ready.","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#reusing-text-snippets","section":"Reusing Text Snippets","audience":"portal","headings":[],"text":"1. Open the **Text** tab in the left panel — this holds reusable text snippets rather than the current page's content.\n\n   [Screenshot: The Text tab in the left panel, listing reusable text snippets]\n\n2. Click a snippet to add it to the current page.\n3. It appears as a normal text box on the canvas, ready to reposition and edit like any other.\n\n   [Screenshot: A text snippet added to the canvas as a normal text box]","agent_tools":[]},{"title":"Adding and Styling Text","url":"https://docs.typograph.nl/portal/editor/text#next-steps","section":"Next Steps","audience":"portal","headings":[],"text":"Continue with [Adding Shapes and Images](https://docs.typograph.nl/portal/editor/shapes-and-images).","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login","section":null,"audience":"portal","headings":[],"text":"Before you can use Typograph, you need to sign in to your account. Pick whichever login method matches your setup — Google, Microsoft, or email — and follow the steps below to reach the Portal.\n\n[Screenshot: The sign in screen with Google, Microsoft, and email options, and the language switch English | Nederlands at the bottom]","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login#choose-a-login-method","section":"Choose a login method","audience":"portal","headings":["Google","Microsoft","Email"],"text":"### Google\n\nSelect **Continue with Google**, then enter the credentials for your personal or business Google account when prompted.\n\n### Microsoft\n\nSelect **Continue with Microsoft**, then enter the credentials for your personal or business Microsoft account when prompted.\n\n### Email\n\nEnter your account's email address — the one you signed up with, or the one you [changed it to](https://docs.typograph.nl/portal/workspace/account#changing-your-email-address) — and select **Continue**. Typograph sends a 6-digit verification code to that address — enter the code to continue.","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login#choosing-your-language","section":"Choosing your language","audience":"portal","headings":[],"text":"The sign-in screens are in English or Dutch. Typograph starts in your browser's language when it is one of the two, and in English otherwise. To change it, select **English** or **Nederlands** at the bottom of the screen: the screen switches straight away and stays in that language while you sign in.\n\n[Screenshot: The sign in screen in Dutch, with Nederlands selected at the bottom]\n\nThe language you pick also decides:\n\n- the language of the email with your verification code;\n- the language of a new account, when you sign up: the Portal and your Typograph emails, the welcome email included, use it from then on. You can change it later under [Language](https://docs.typograph.nl/portal/workspace/account#changing-your-preferences) in your profile.\n\nOnce you are signed in, the language of your account counts.","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login#signing-in-with-google-or-microsoft","section":"Signing in with Google or Microsoft","audience":"portal","headings":["When Google or Microsoft hasn't confirmed your address","When your address moved to another Typograph account"],"text":"Typograph remembers the Google or Microsoft account you signed in with, not just its email address. If that account's address changes later, **Continue with Google** or **Continue with Microsoft** still takes you to the same Typograph account.\n\n### When Google or Microsoft hasn't confirmed your address\n\nTypograph only lets you in through Google or Microsoft with an email address the provider has confirmed belongs to you. When it hasn't, you see *We couldn't sign you in with Google* (or Microsoft), with your address filled in:\n\n1. Select **Email me a code**. Typograph sends a 6-digit code to that address.\n2. Enter the code from the email.\n\nYou only do this once. After that, **Continue with Google** or **Continue with Microsoft** signs you in straight away, without a code.\n\nWhen the provider didn't share an email address at all, the same screen asks you to enter it first. Entering a wrong code five times locks sign-in codes for that account for 15 minutes; the screen says how long to wait.\n\n### When your address moved to another Typograph account\n\nAfter an account [changes its email address](https://docs.typograph.nl/portal/workspace/account#changing-your-email-address), the old address stays with that account for 7 days, as long as the **Undo the change** link works. Signing in with Google or Microsoft with the old address in that time shows *This email address moved to another Typograph account*. The screen says from which moment the address is free for an account of its own. Until then:\n\n[Screenshot: The message on the sign-in screen that the email address moved to another Typograph account, with what to do and from when the address is free]\n\n- Changed it yourself? Enter your new address below the message and select **Continue**, or sign in with Google or Microsoft with the new address.\n- Didn't change it yourself? Use the **Undo the change** link in the email sent to this address at the time; see [Didn't change it yourself?](https://docs.typograph.nl/portal/workspace/account#didnt-change-it-yourself).\n\nTypograph shows this only when Google or Microsoft confirmed that the address is yours. Anyone else who tries the old address learns nothing from the screen; the address itself gets an email that says the same.","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login#adding-your-name","section":"Adding your name","audience":"portal","headings":[],"text":"Every Typograph account has a first and a last name, so the people you work with know who you are. When Google or Microsoft didn't share one of them, Typograph asks for the missing part right after you sign in, and for your whole name when it has neither. Enter it and select **Continue** to go on. Not the right account? **Sign out** takes you back.\n\n[Screenshot: The name step asking Sam for a last name, with a Continue button and a Sign out link]","agent_tools":[]},{"title":"Signing In","url":"https://docs.typograph.nl/portal/getting-started/login#granting-portal-ui-access","section":"Granting Portal UI access","audience":"portal","headings":[],"text":"Whichever method you use, you'll be asked to approve access once your credentials are verified: a prompt lists the permissions Portal UI needs to connect to your Typograph account. Read through them before continuing.\n\nNot comfortable granting these permissions? Check our [Terms of Service](https://typograph.nl/terms) and [Privacy Policy](https://typograph.nl/privacy) to see how your information is used and protected, then decide whether to proceed.\n\nOnce you approve, you're signed in and taken straight to the Portal.","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation","section":null,"audience":"portal","headings":[],"text":"The Portal is organized around two primary navigation surfaces: the **top bar** for organization-wide tools and the **left sidebar** for your content and shortcuts.","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#top-bar","section":"Top Bar","audience":"portal","headings":[],"text":"| Element | What it does |\n|---------|--------------|\n| **[Search](https://docs.typograph.nl/portal/workspace/search)** | Find templates, projects, teams, media, fonts and items in the trash by name |\n| **Notifications** | Mentions, invites, activity |\n| **Account menu** (top right) | Profile, [Organizations](https://docs.typograph.nl/portal/organization/structure#opening-another-organization), help, sign out |","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#sidebar","section":"Sidebar","audience":"portal","headings":[],"text":"The sidebar is organized top-to-bottom:\n\n| Section | What it contains |\n|---------|------------------|\n| **Recents** | Recently opened templates |\n| **Favorites** | Favorite templates/projects (toggle the heart icon to add/remove) |\n| **Teams** | Your teams, grouped by organization when you have teams in more than one (see [Teams of another organization](https://docs.typograph.nl/portal/getting-started/navigation#teams-of-another-organization)) |\n| **Shared with me** | Shared Projects and Shared Templates from other teams/orgs |\n| **Trash** | Items you or teammates have soft-deleted, to restore within 30 days (see [Trash](https://docs.typograph.nl/portal/workspace/trash)) |","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#teams-of-another-organization","section":"Teams of Another Organization","audience":"portal","headings":[],"text":"Someone can invite you to a team of an organization you are not a member of. You then work in that team, but not in the rest of their organization.\n\nWhen your teams belong to more than one organization, the sidebar groups them under the organization's name: your own organizations first, then the others. An organization you are not a member of is marked **External**. With teams in one organization only, the sidebar lists them as before, without a heading.\n\nOpen a team of another organization, and its page names that organization above the team's name, marked **External**, with a note that the team belongs to that organization: what you make there is theirs, and their administrators decide who has access.\n\n[Screenshot: The page of Walk's team. The sidebar lists the teams under Studio Noord and Typograph Development, then under Walk's Organization, marked External. Above the team's name the breadcrumb reads Walk's Organization, External, then Walk's team, and a note says: This team belongs to Walk's Organization. You are a member of the team, not of the organization. What you make here belongs to Walk's Organization, and its administrators decide who has access.]","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#working-down-the-hierarchy","section":"Working Down the Hierarchy","audience":"portal","headings":[],"text":"1. **Pick an organization** in the top-left picker.\n2. **Pick a team** in the sidebar.\n3. **Pick a project** (team → projects list, or create a new project).\n4. Inside a project you see **Templates**, **Media**, **Fonts**, **Clips** and **Forms** tabs.\n5. **Click a template** to open it in the Editor.","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#going-back-up-from-a-project","section":"Going Back Up From a Project","audience":"portal","headings":[],"text":"Above a project's name, a breadcrumb shows where the project lives. Click the team's name to go back to the team's projects.\n\n[Screenshot: The header of the project Demo project, with the breadcrumb Demo team, then Demo project, above its name, and the buttons New template and Share on the right]\n\nIf you were invited to the project but are not a member of its team, the breadcrumb starts at **Shared with me** instead, where the project is listed for you.\n\n[Screenshot: The header of the project Demo project for someone invited to the project alone, with the breadcrumb Shared with me, then Demo project, above its name]\n\nOn a phone the breadcrumb works the same way. If you can edit the project, its buttons sit in one row under its name, and **Share** shows everyone with access.","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#organization-tools","section":"Organization Tools","audience":"portal","headings":[],"text":"Under **Organizations → {Your Org}**:\n\n- Members, Teams, Apps (OAuth clients), Webhooks, Subscription, Usage","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#account-tools","section":"Account Tools","audience":"portal","headings":[],"text":"Under your account menu:\n\n- **Profile** — name, email, preferences\n- **Language** — switch the UI language","agent_tools":[]},{"title":"Navigation","url":"https://docs.typograph.nl/portal/getting-started/navigation#shortcuts","section":"Shortcuts","audience":"portal","headings":[],"text":"Keyboard shortcuts vary with context. The Editor exposes its own shortcut panel (press `?` inside the Editor). Outside the Editor, most navigation is through the UI. The Portal-wide shortcuts open search: <kbd>/</kbd> from anywhere outside a text field, and <kbd>Ctrl</kbd>+<kbd>K</kbd> (<kbd>⌘</kbd>+<kbd>K</kbd> on a Mac) from anywhere ([Searching with the Keyboard](https://docs.typograph.nl/portal/workspace/search#searching-with-the-keyboard)).","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview","section":null,"audience":"portal","headings":[],"text":"The Typograph Portal is the web UI for designers and content operators. Everything you can do through the API is also reachable through the Portal, plus collaborative affordances like sharing, invites, comments, favorites, and trash.","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview#core-hierarchy","section":"Core Hierarchy","audience":"portal","headings":[],"text":"The Portal organizes content as:\n\n```\nOrganization\n  └─ Teams\n       └─ Projects\n            ├─ Templates   (the design files)\n            ├─ Media       (images and assets used in templates)\n            └─ Fonts       (fonts used in templates)\n```\n\n- You can belong to **multiple organizations** (e.g. personal + employer). Switch between them in the top-left org picker.\n- An organization has **teams**, which group collaborators.\n- Teams have **projects**, which bundle the templates and their supporting assets.\n- Projects own **templates**, **media**, and **fonts** — these three always scope to a specific project.","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview#when-you-sign-up","section":"When You Sign Up","audience":"portal","headings":[],"text":"Your new account starts with an organization, a team of your own and a project called **First edition ⭐**. In that project is a welcome card with your first name on it, already among your [favorites](https://docs.typograph.nl/portal/workspace/favorites). Open it to try the Editor.","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview#main-features","section":"Main Features","audience":"portal","headings":[],"text":"| Feature | What it does |\n|---------|--------------|\n| **Templates** | Create, edit, version, and share Typograph designs (opens the Editor) |\n| **[Media & Fonts](https://docs.typograph.nl/portal/content/assets)** | Upload images and fonts scoped to a project |\n| **[Sharing / Invites](https://docs.typograph.nl/portal/organization/invites)** | Invite people into your organization, or to a team, project, or template, with a specific role |\n| **Comments** | Leave and resolve comments on a template |\n| **[Favorites](https://docs.typograph.nl/portal/workspace/favorites)** | Mark templates/projects you come back to often with the heart icon |\n| **Recents** | Quick access to recently opened templates and searches |\n| **Shared with me** | Projects and templates other people have shared with you |\n| **[Trash](https://docs.typograph.nl/portal/workspace/trash)** | Soft-deleted items — restorable for 30 days from the trash in the sidebar |\n| **[Notifications](https://docs.typograph.nl/portal/workspace/notifications)** | Mentions, invites, and activity updates |\n| **[Organization settings](https://docs.typograph.nl/portal/organization/structure)** | Members, OAuth apps, webhooks, subscription, usage |","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview#getting-in","section":"Getting In","audience":"portal","headings":[],"text":"Sign-in is handled by the separate **Auth UI** service. The Portal redirects you there automatically — sign in with Google, Microsoft, or a verification code sent to your email, and you're returned to the Portal. See [Signing In](https://docs.typograph.nl/portal/getting-started/login) for the steps. Your access token is managed for you; you don't need to handle it manually.","agent_tools":[]},{"title":"Portal Overview","url":"https://docs.typograph.nl/portal/getting-started/overview#organization-level-tools","section":"Organization-Level Tools","audience":"portal","headings":[],"text":"Members of an organization (with the right role) can reach these under **Organizations → {Your Org}** in the top nav:\n\n- **Members** — change members' roles and remove members (see [Managing Members](https://docs.typograph.nl/portal/organization/members))\n- **[Apps](https://docs.typograph.nl/portal/organization/apps)** — OAuth clients for programmatic API access\n- **[Webhooks](https://docs.typograph.nl/portal/organization/webhooks)** — subscribe external services to Typograph events\n- **[Subscription & Usage](https://docs.typograph.nl/portal/organization/billing)** — current plan, change plan, rate-limit usage across categories","agent_tools":[]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps","section":null,"audience":"portal","headings":[],"text":"An **App** connects an outside tool or a piece of software your organization uses to Typograph, so it can work with your projects and templates on your behalf. You'll need one before connecting anything external — a custom integration, an internal tool, or a [webhook](https://docs.typograph.nl/portal/organization/webhooks).\n\nOnly an owner or admin of the organization can create or delete apps, or change an app's logo or IP addresses. Members see the list and each app's details; the **New App**, **Logo**, **IP addresses** and **Delete** actions are not shown to them. See [Permissions](https://docs.typograph.nl/portal/users/permissions).","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps#creating-an-app","section":"Creating an App","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Apps**.\n2. Click **New App**.\n3. Give the app a name — this is how it'll show up in your app list.\n4. Add its **redirect URIs** — the web addresses Typograph is allowed to send you back to once you've approved the connection. Add one for each place the app runs (for example, your test environment and your live one).\n5. Select the **scopes** the app needs — these are the specific permissions it's asking for. Only grant what the integration actually needs.\n6. Save. The app now has a client ID and secret it can use to connect.","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps#viewing-or-removing-an-app","section":"Viewing or Removing an App","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Apps**.\n2. Select the app to see its details — name, redirect URIs, and granted permissions.\n3. Click **Delete** to remove it.\n\nDeleting an app immediately cuts off its access. Anything that was using it — including any [webhook](https://docs.typograph.nl/portal/organization/webhooks) attached to it — stops working.","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps#giving-an-app-a-logo","section":"Giving an App a Logo","audience":"portal","headings":[],"text":"An app that people sign in to (one with a redirect URI) shows its logo on the consent screen, where they decide whether to give it access to their Typograph account. Without one, they see a grey placeholder.\n\n1. Go to **Organizations → {Your Org} → Apps**.\n2. Open the **⋯** menu of the app and choose **Logo**.\n3. Choose a **square** PNG, JPG or WebP image of **128 to 1024 pixels per side**, at most **512 KB**. 256 × 256 pixels looks sharp everywhere. The portal shows the image before you save and says which requirement it misses.\n4. Click **Save logo**. **Remove logo** brings back the placeholder.\n\nTypograph does not crop or resize the image, so make it square yourself, with some space around the mark. More detail for developers: [API → Authentication](https://docs.typograph.nl/guides/api/authentication#app-logo).","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps#limiting-an-app-to-ip-addresses","section":"Limiting an App to IP Addresses","audience":"portal","headings":[],"text":"An app that runs on your own server can be limited to the addresses that server uses. Its client ID, secret and tokens then work from those addresses only, so a secret that leaks is of no use anywhere else.\n\n1. Go to **Organizations → {Your Org} → Apps**.\n2. Open the **⋯** menu of the app and choose **IP addresses**.\n3. Write the addresses, **one per line**: a single address such as `203.0.113.7`, or a range such as `203.0.113.0/24`. IPv4 and IPv6 both work, up to 50 lines.\n4. Click **Save addresses**.\n\nWithin a minute the app stops working from any other address, also with tokens it already has. Ask whoever runs the app which addresses its server sends from before you save. **Allow every address** lifts the limit again. The app's details show the list, or **Any address**.\n\nThe portal shows your own address as you open the window. Add it only when the app runs on the network you are on, for instance while you test it.\n\nAn app that runs in a browser or on a phone (one without a secret) has no **IP addresses** action: its users' addresses cannot be listed. More detail for developers: [API → Authentication](https://docs.typograph.nl/guides/api/authentication#ip-allowlist).","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"Apps","url":"https://docs.typograph.nl/portal/organization/apps#related","section":"Related","audience":"portal","headings":[],"text":"- [Webhooks](https://docs.typograph.nl/portal/organization/webhooks) — the next step once an app exists\n- [AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant) — what the portal's own assistant may do in your organization, on its **Assistant** tab\n- [API → Authentication](https://docs.typograph.nl/guides/api/authentication) — technical details for developers building against an app\n- [API → Scopes](https://docs.typograph.nl/guides/api/scopes) — what each permission grants","agent_tools":["app_list","app_get","app_logo_upload","app_logo_delete","scope_list"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant","section":null,"audience":"portal","headings":[],"text":"The portal's [assistant](https://docs.typograph.nl/portal/workspace/assistant) works for each person in every organization they belong to, as a member or as a guest. Your organization decides what it may do here, for everyone who works here, and whose AI credits it may use. The settings are on the organization's **Assistant** tab. Every member sees them; only owners and admins can change them. The tab appears once the assistant is available to your organization.\n\n1. Go to **Organizations → {Your Org} → Assistant**.\n2. Choose what the assistant may do, and whether guests may use your credits, as described below.","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant#what-the-assistant-may-do-here","section":"What the Assistant May Do Here","audience":"portal","headings":["When It Takes Effect"],"text":"One setting, with three choices:\n\n| Choice | The assistant |\n|---|---|\n| **Read and edit** (the default) | Searches and reads, and changes things after a plan the person confirms, with their own rights |\n| **Read only** | Searches and reads; people make changes in the portal themselves |\n| **Off** | Reads and changes nothing here |\n\nWhatever you choose, the assistant never does more than the person may do in the portal. A viewer can't change anything through it, in any of the three.\n\n**Off** is under **Privacy**. It is part of the Enterprise plan: on Free and Pro you see it greyed out, with the plan that includes it and **See the plans**. A member who is no owner or admin reads that they can ask an admin to upgrade. An organization whose assistant was turned off before this setting existed keeps **Off**, on every plan.\n\n### When It Takes Effect\n\nA lower setting takes effect within a minute, for everyone:\n\n- **Read only**: a change the assistant runs after that is refused, also a change in a plan the person proposed earlier, and the assistant says that the person makes it in the portal. Undoing a change it made before works the same way: it is refused, with that explanation.\n- **Off**: the assistant no longer searches here. When a question could have found something here, it names your organization as one it skipped, and points to the portal. The results and cards it showed earlier from your organization disappear from its conversations and are no longer read back to the AI; answers it wrote then stay as they were.","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant#ai-provider","section":"AI Provider","audience":"portal","headings":[],"text":"The assistant's AI is **Anthropic · Claude**. It processes what the assistant reads and writes for the people working here. **How we process data** explains what is sent and how it is kept. There is one provider, so there is nothing to choose; to keep your organization's data from the provider, turn the assistant **Off**.","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant#credits","section":"Credits","audience":"portal","headings":[],"text":"Each turn of a conversation is paid by one organization, in [AI credits](https://docs.typograph.nl/portal/workspace/assistant#ai-credits). Your organization pays only for work in your organization:\n\n- **Editors and admins** of this organization use its credits for work here: people with **Can edit** or **Admin** on a team or anything in it, and the organization's owner and admins.\n- **Viewers and commenters** never use them: they pay with an organization of their own. So inviting reviewers costs you no credits.\n- **Guests**, people with a role on your teams who are not members, use their own organization's credits, unless you turn on **Guests may use our credits for work here**. It is off by default. On, a guest who edits in your teams can let your organization pay for work in it; it never pays for their work elsewhere.\n\nWho used how many credits this month is on the organization's [usage page](https://docs.typograph.nl/portal/organization/billing#ai-credits-per-person).","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant#through-the-api-and-the-mcp-server","section":"Through the API and the MCP Server","audience":"portal","headings":[],"text":"The same settings are `GET` and `PUT /v1/identity/organizations/{organizationId}/assistant` ([Identity Service](https://docs.typograph.nl/guides/api/identity-service#the-assistant)), and the MCP tools `organization_assistant_get` and `organization_assistant_update`, which asks for confirmation first.","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"AI Assistant Settings","url":"https://docs.typograph.nl/portal/organization/assistant#related","section":"Related","audience":"portal","headings":[],"text":"- [Assistant](https://docs.typograph.nl/portal/workspace/assistant) — what the assistant does, and who pays a turn\n- [Apps of Guests](https://docs.typograph.nl/portal/organization/guest-apps) — what apps that guests connect may read; the portal's assistant follows the setting above instead\n- [Subscription & Usage](https://docs.typograph.nl/portal/organization/billing#checking-your-usage) — the AI credits of your plan, and who used them","agent_tools":["organization_assistant_get","organization_assistant_update"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing","section":null,"audience":"portal","headings":[],"text":"Every organization is on a plan — **Free**, **Pro**, or **Enterprise** — that determines how much you can use Typograph before hitting a limit.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#viewing-your-plan","section":"Viewing Your Plan","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Subscription**.\n\nYou'll see the organization's current plan and what it includes.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#changing-your-plan","section":"Changing Your Plan","audience":"portal","headings":[],"text":"Free costs nothing. Pro and Enterprise are on request: they have no published price, and you change to them by asking.\n\n1. From the Subscription page, click **Change plan**.\n2. Compare the plans.\n3. Choose **Ask for Pro** on Pro, or **Talk to an expert** on Enterprise, fill in the form, and send it.\n\nThe sales team gets back to you, agrees the change and its price with you, and then moves your organization to the new plan. Nothing changes until then.\n\nAny member of the organization can send the request. Add-ons and cancelling remain the Owner's: see [User Roles](https://docs.typograph.nl/portal/users/roles).","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#one-free-organization-per-person","section":"One Free Organization per Person","audience":"portal","headings":[],"text":"Every organization on the Free plan has limits of its own, so you own **one organization on the Free plan**. A new organization starts on Free, so while you own a free one, creating another is refused, and the page names the organization you have. To create another:\n\n- move the organization you have to a paid plan, from its **Change plan** page, or\n- ask us to set up the new organization on a paid plan.\n\nOnly the organizations you own count, not the ones you were invited to. On the Free plan, publishing and converting also count per person, across every free organization you work in, so working in several free organizations does not raise your limits.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#which-organization-your-work-counts-for","section":"Which Organization Your Work Counts For","audience":"portal","headings":[],"text":"- **In the portal**, browsing, editing, sharing and the rest of your work count toward none of your request limits. Of the requests, publisher and converter work counts, for the organization that owns the project or template. That holds also when someone from another organization does it, in a team you invited them to: give them a lower role if they should not. Apart from that, the files you store count toward the organization's **Storage**, and the [assistant](https://docs.typograph.nl/portal/workspace/assistant) counts its requests in the `ai` category and is paid from AI credits: each turn by one organization, the one of the work when the person is an editor or admin there, as [Who Pays a Turn](https://docs.typograph.nl/portal/workspace/assistant#who-pays-a-turn) explains.\n- **Through the API and the MCP server**, every request counts for the organization the app's token was granted for: the one chosen when signing in to the app, or the one that owns the app. An app that someone from another organization connects only reads in your organization, if you allow it at all ([Apps of guests](https://docs.typograph.nl/portal/organization/guest-apps)), so it never counts for yours.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#teams-and-editor-seats","section":"Teams and editor seats","audience":"portal","headings":[],"text":"Each plan sets how many teams the organization can have, and how many people can edit in it (its editor seats). The **Change plan** page lists both for every plan:\n\n[Screenshot: The plan comparison under General: Teams 1 on Free and Unlimited on Pro and Enterprise; Editor seats (edit/admin) 1 on Free, 5 on Pro and 10 on Enterprise; Invited users (view/comment) Unlimited on every plan]\n\n| Plan | Teams | Editor seats |\n|---|---|---|\n| **Free** | 1 | 1, the owner's |\n| **Pro** | No limit | 5 |\n| **Enterprise** | No limit | 10 |\n\nPeople who can only view or comment don't take a seat, on any plan. Your organization's own numbers are on its [Teams](https://docs.typograph.nl/portal/organization/structure#when-your-plan-has-no-room-for-another-team) and [Members](https://docs.typograph.nl/portal/organization/members#editor-seats) pages; once they are all in use, the portal says so where you would add a team or someone who edits, and offers the way on.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#checking-your-usage","section":"Checking Your Usage","audience":"portal","headings":["AI credits per person","Storage and the trash"],"text":"1. Go to **Organizations → {Your Org} → Usage**.\n\nYou'll see your consumption broken down by category, each measured against your plan's limit:\n\n| Category | Covers |\n|----------|--------|\n| **General** | Overall activity |\n| **File** | Working with projects, templates, and media |\n| **Publisher** | Publishing and exporting templates |\n| **Converter** | Converting files between formats |\n| **Storage** | The files stored across the organization: each image at its largest version, fonts, templates and clips. An image counts at its original or its print version, whichever is larger, which for a photo is usually the original; its screen version and thumbnail do not count |\n| **Trash** | The files in the [trash](https://docs.typograph.nl/portal/workspace/trash), next to their allowance; see [Storage and the trash](https://docs.typograph.nl/portal/organization/billing#storage-and-the-trash) |\n| **Webhooks** | Webhook notifications sent |\n| **AI credits** | What the [assistant](https://docs.typograph.nl/portal/workspace/assistant#ai-credits) used this month, against the plan's AI credits plus any [credit bundle](https://docs.typograph.nl/portal/workspace/assistant#requesting-extra-credits), and when they start again; and who used them, see [AI credits per person](https://docs.typograph.nl/portal/organization/billing#ai-credits-per-person) |\n\nA category nearing its limit is highlighted, and the page offers **Upgrade plan**, which opens the plan change. A category that's reached its limit blocks further use in that category until it resets or you switch to a bigger plan.\n\n### AI credits per person\n\nThe **AI credits** card also shows what is left this month and today, and who used how many credits this month:\n\n- **Owners and admins** see everyone, with guests listed apart: people with a role on your teams who are not members, who use your credits only when your [AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant#credits) let them.\n- **Other members** see their own credits and the organization's total.\n\nThere is no limit per person: anyone who may use the organization's credits can use its whole day. Once 80% of the month is used, the card says who used the most, and the owner and admins get one email about it that month, so you can agree who does what, or add a [credit bundle](https://docs.typograph.nl/portal/workspace/assistant#requesting-extra-credits), before the credits run out.\n\n### Storage and the trash\n\nFiles you delete stay in the [trash](https://docs.typograph.nl/portal/workspace/trash) for 30 days, so they can still be restored. The usage page has a **Trash** card beside **Storage**: how much the trash holds, next to its allowance, which is as much as your plan's storage.\n\n- **Within the allowance**, files in the trash don't count toward your storage. Deleting a file frees its room at once, without anyone having to empty the trash.\n- **Beyond the allowance**, the rest counts toward your storage, and the Trash card says how much.\n\nAfter 30 days, files in the trash are deleted for good and stop taking up room.","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Subscription & Usage","url":"https://docs.typograph.nl/portal/organization/billing#related","section":"Related","audience":"portal","headings":[],"text":"- [User Roles](https://docs.typograph.nl/portal/users/roles) — who can view or change billing\n- [Apps](https://docs.typograph.nl/portal/organization/apps) and [Webhooks](https://docs.typograph.nl/portal/organization/webhooks) — integrations whose usage counts toward these limits","agent_tools":["subscription_plan_list","subscription_plan_get"]},{"title":"Creating an Organization","url":"https://docs.typograph.nl/portal/organization/create-organization","section":null,"audience":"portal","headings":[],"text":"You get an organization when you sign up. Create another when work belongs apart: another company, a client, a brand of its own. Each organization has its own members, teams, apps, webhooks, subscription and billing, and its own usage limits, apart from your other organizations.","agent_tools":["organization_create"]},{"title":"Creating an Organization","url":"https://docs.typograph.nl/portal/organization/create-organization#creating-one","section":"Creating one","audience":"portal","headings":[],"text":"1. Open your avatar menu at the top right and choose **Organizations**.\n2. Click **New organization**.\n3. Enter the **Organization name**: the company or client it is for. You can change it later in the settings.\n4. Leave the **URL slug** empty to have one made from the name. Under the field the portal shows the slug it will become, such as `studio-noord`. Fill it in only when you want another: at least 3 lowercase letters, digits and single hyphens.\n\n   [Screenshot: The New organization page with Studio Noord as the name, an empty URL slug field whose help says Leave it empty to use studio-noord, and the buttons Create organization and Cancel]\n\n5. Click **Create organization**.\n\nThe new organization opens on its **Settings**, and a message says you are its owner. It starts on the free plan: see [Plans & Billing](https://docs.typograph.nl/portal/organization/billing) to change that.\n\nA new organization has no contact email yet, and its settings save only with one: a notice above the form says so. Click **Add a contact email**, enter the address that should get important notices about the organization, and click **Save**. Fields marked with an asterisk are required.\n\n[Screenshot: The Settings of the new organization Studio Noord, with the notice This organization has no contact email yet, and the settings only save with one, the link Add a contact email, and an empty Contact email field marked as required]\n\nNext, [invite people](https://docs.typograph.nl/portal/organization/invites) or create a team.","agent_tools":["organization_create"]},{"title":"Creating an Organization","url":"https://docs.typograph.nl/portal/organization/create-organization#when-the-portal-says-no","section":"When the portal says no","audience":"portal","headings":[],"text":"- **Another organization already uses this URL slug.** Choose another, or empty the field to have one made from the name; a slug made from the name never clashes. When you change the slug later in the **Settings**, the message is under the field: choose another and click **Save** again.\n- **A field is marked.** The organization needs a name, and a slug you type needs at least 3 characters: lowercase letters, digits and single hyphens.\n- **Something went wrong while creating the organization.** Look at your organizations first: if the new one is listed, it was created after all. If not, try again. Still stuck? [Contact support](https://docs.typograph.nl/guides/support).","agent_tools":["organization_create"]},{"title":"Creating an Organization","url":"https://docs.typograph.nl/portal/organization/create-organization#when-you-are-in-no-organization","section":"When you are in no organization","audience":"portal","headings":[],"text":"When the last organization you belonged to is deleted, the **Organizations** page says you are not in an organization and offers **Create an organization**. Create one to carry on, or ask someone to invite you to theirs. Your account stays as it is.\n\n[Screenshot: The Organizations page for someone in no organization: You are not in an organization, with the button Create an organization]\n\nA page of an organization that no longer exists, such as a bookmark to its settings, brings you to **Organizations** with a message that the organization is gone.","agent_tools":["organization_create"]},{"title":"Creating an Organization","url":"https://docs.typograph.nl/portal/organization/create-organization#from-an-agent-or-the-api","section":"From an agent or the API","audience":"portal","headings":[],"text":"An agent can create an organization with the MCP tool `organization_create`, and an integration with `POST /v1/identity/organizations`. Both need the `identity` or `identity:write` scope. An AI client that connected to Typograph on its own, such as Claude Desktop, does not get that scope, so it sends you here instead. See the [MCP server](https://docs.typograph.nl/guides/api/mcp-server) and [Identity Service](https://docs.typograph.nl/guides/api/identity-service) references.","agent_tools":["organization_create"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization","section":null,"audience":"portal","headings":[],"text":"The owner of an organization can delete it. You confirm it with a code we email you, so nobody deletes an organization without access to the owner's mailbox. You cannot undo it yourself. Deleted it by mistake? [Support](mailto:support@typograph.nl) can restore it within 30 days; after that its teams, projects, templates and files are deleted for good.\n\nYou always keep at least one organization. Your only organization cannot be deleted: create another one first, or ask someone to invite you to theirs.","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#what-happens-when-you-delete-it","section":"What happens when you delete it","audience":"portal","headings":[],"text":"- Everyone in the organization, you included, loses access to it at once.\n- Nobody can open its teams, projects, templates, forms and files any more. They are not in anyone's [Trash](https://docs.typograph.nl/portal/workspace/trash), and 30 days later they are deleted for good, the files and the answers entered in the forms included.\n- Its apps stop working, and so does everything connected through them: forms embedded on your sites, API integrations and connected AI assistants. Its [webhooks](https://docs.typograph.nl/portal/organization/webhooks) are deleted.\n- Its subscription ends at once. A paid plan ends too, without waiting for the end of the period. Questions about your invoice? [Contact support](https://docs.typograph.nl/guides/support).\n\nYour other organizations, and your account, stay as they are. Want someone else to carry on with the organization instead? [Transfer the ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) to them.","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#deleting-the-organization","section":"Deleting the organization","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Settings**. Under **Delete organization**, click **Delete organization…**. Only the owner sees this card.\n\n   [Screenshot: Organization Settings with the Delete organization card below Ownership, and the button Delete organization…]\n\n2. Read what happens, and click **Email me a code**. Nothing is deleted yet.\n\n   [Screenshot: The page Delete Demo Print Shop, listing what happens when the organization is deleted, with the buttons Email me a code and Cancel]\n\n3. Open the email, titled *Your code to delete the organization {name}*. Enter the 6-digit code from it, and click **Delete {name}**. The code works for 15 minutes, and for this organization only.\n\n   [Screenshot: The page Delete Demo Print Shop with the field Code from the email and the button Delete Demo Print Shop]\n\nThe organization is deleted, and your organizations are listed with a message that says which one went, that its teams, projects, templates and files are deleted for good in 30 days, and that support can restore it until then.\n\n[Screenshot: The Organizations page with the message Demo Print Shop is deleted. Nobody has access to it any more, and we delete its teams, projects, templates and files for good in 30 days. Until then, support can restore it. Need help? Contact support]","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#when-it-cannot-be-deleted","section":"When it cannot be deleted","audience":"portal","headings":[],"text":"| What you see | Why | What to do |\n|---|---|---|\n| The card offers **Create another organization** instead of a delete button | It is the only organization you are in. | Create another organization, or ask someone to invite you to theirs, and come back. |\n| No **Delete organization** card in the settings | Only the owner can delete an organization. | Ask the owner, who is named under **Ownership**. |\n| *That code is wrong, or it has expired* | The code was mistyped, is older than 15 minutes, or a newer code replaced it. | Check the latest email, or click **Send a new code**. |\n| *Too many wrong codes* | After several wrong codes, entering a code is paused. | Wait a few minutes, then send a new code. |\n| *To delete this organization, sign in again first* | You signed in before deleting organizations was possible. | Click **Sign in again**. You come back to the same page. |\n\n[Screenshot: The page Delete Demo's Organization, saying it is the only organization you are in and you always keep at least one, with the button Create another organization]","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#deleted-it-by-mistake","section":"Deleted it by mistake?","audience":"portal","headings":[],"text":"Within 30 days, support can restore the organization. The owner writes to [support@typograph.nl](mailto:support@typograph.nl) from the owner's email address, naming the organization. When it is restored:\n\n- Its members, teams, projects, templates and files are back, with everyone's access as it was.\n- Its subscription is back as it was, the same plan.\n- Everyone signs in again, and its apps have to be connected again, with their [webhooks](https://docs.typograph.nl/portal/organization/webhooks).\n- Invitations that were still open are gone: send them again.\n\nAfter 30 days the content is deleted for good, and the organization can no longer be restored. Neither you nor an AI agent can restore an organization yourselves: only support can.","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#over-the-api-and-with-an-ai-agent","section":"Over the API and with an AI agent","audience":"portal","headings":[],"text":"Deleting works the same way outside the portal, with the same code:\n\n- **API:** call `POST /v1/identity/organizations/{organizationId}/deletion-code`, then `DELETE /v1/identity/organizations/{organizationId}` with the code. Both need the `identity:delete` scope. See [Identity Service → Delete an organization](https://docs.typograph.nl/guides/api/identity-service#delete-an-organization).\n- **AI agent:** the [MCP server](https://docs.typograph.nl/guides/api/mcp-server#organizations) tools `organization_deletion_code` and `organization_delete`. The agent asks you for the code from the email.","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Deleting an Organization","url":"https://docs.typograph.nl/portal/organization/delete-organization#related","section":"Related","audience":"portal","headings":[],"text":"- [Transferring Ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) — handing the organization to another member instead\n- [Billing](https://docs.typograph.nl/portal/organization/billing) — the subscription that ends with the organization\n- [User Roles](https://docs.typograph.nl/portal/users/roles) — what an owner, admin and member can do","agent_tools":["organization_deletion_code","organization_delete"]},{"title":"Apps of Guests","url":"https://docs.typograph.nl/portal/organization/guest-apps","section":null,"audience":"portal","headings":[],"text":"People from other organizations can work in your teams. In Typograph they are **guests**: they have a role in one of your teams, but they are not members of your organization. The portal shows them your teams under your organization's name.\n\nA guest can also connect an app, such as an AI assistant through the [MCP server](https://docs.typograph.nl/guides/api/mcp-server). That app is tied to the guest's own organization, so it does not see your teams. You decide whether it may.\n\nThe portal's own [assistant](https://docs.typograph.nl/portal/workspace/assistant) is no such app: it works for the guest with their own rights, and follows your [AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant) instead of this page.","agent_tools":["organization_guest_apps_get","organization_guest_apps_update"]},{"title":"Apps of Guests","url":"https://docs.typograph.nl/portal/organization/guest-apps#letting-the-apps-of-guests-read","section":"Letting the Apps of Guests Read","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Settings**.\n2. Under **Apps of guests**, turn on **Let the apps of guests read**.\n\nFrom then on, an app a guest connects can read the teams, projects and templates that guest works on in your organization, and make PDFs of them. It never changes anything in your teams, whatever the guest's role there. Its PDFs and other jobs are billed to the guest's own organization, not to yours.\n\nTurn the switch off again and those apps stop reading within a minute. It is off until you turn it on.\n\nOnly an owner or admin sees **Apps of guests** and can change it. See [Permissions](https://docs.typograph.nl/portal/users/permissions).","agent_tools":["organization_guest_apps_get","organization_guest_apps_update"]},{"title":"Apps of Guests","url":"https://docs.typograph.nl/portal/organization/guest-apps#seeing-and-blocking-the-apps-that-read","section":"Seeing and Blocking the Apps That Read","audience":"portal","headings":[],"text":"Under the switch, **Apps that read for a guest** lists every app that was let read your teams, the latest first, with where it comes from and when it last read.\n\nAn app picks its own name, so two apps can both be called \"Claude\". Where it comes from tells them apart:\n\n- **From claude.ai**: the app registered itself, and its details live at that address. Only the company behind that address can put an app there.\n- **Registered in Typograph**: an organization created the app in Typograph.\n\n[Screenshot: The Apps of guests card with the switch Let the apps of guests read turned on. Under Apps that read for a guest: Claude, from claude.ai, allowed, with a Block button; another app also called Claude, from claude-tools.example.net, blocked, with an Allow button; and Drukkerij Koppeling, registered in Typograph, allowed]\n\n- Click **Block** to stop an app. Typograph asks you to confirm first, naming the app and where it comes from, because blocking an app also signs out the members of your organization who use that same app here.\n- A blocked app shows **Blocked**. Click **Allow** to let it in again.\n\nA blocked app reads nothing in your organization, also while the switch is on.","agent_tools":["organization_guest_apps_get","organization_guest_apps_update"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites","section":null,"audience":"portal","headings":[],"text":"You can grant access to your work at three different levels — organization, team, and project — and they don't have to match. A single template can be shared too; that is covered in [Managing Templates](https://docs.typograph.nl/portal/content/templates). Someone can be a full member of your organization with no access to a specific project, or the reverse: a guest with no organization role at all, invited into a single project.","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites#inviting-someone-to-your-organization","section":"Inviting Someone to Your Organization","audience":"portal","headings":["Pending invitations","When an invitation cannot be sent"],"text":"The owner and admins of an organization invite people into it. The person gets an email with a link; they become a member once they accept. Someone without a Typograph account creates one on the way, with the address you invited.\n\n1. Go to **Organizations → {Your Org} → Members**.\n2. Click **Invite**.\n3. Enter the person's email address. Only an account with this address can accept the invitation.\n4. Pick a role:\n\n   | Role | Access |\n   |------|--------|\n   | **Member** | Works in the teams and projects they are added to |\n   | **Admin** | Also manages members, apps, webhooks and settings |\n\n   [Screenshot: The Invite someone dialog with the address alex.demo@example.com entered and the Member role selected]\n\n5. Click **Send invitation**.\n\nA message confirms that the invitation was sent, and the person appears under **Pending invitations**, below the members. The link in the email works for 7 days.\n\nThe [assistant](https://docs.typograph.nl/portal/workspace/assistant#letting-the-assistant-do-it-for-you) can send this invitation for you too, after a plan you approve, when you are an owner or admin of the organization and its [assistant setting](https://docs.typograph.nl/portal/organization/assistant) is **Read and edit**.\n\nThe invite form does not offer the **Owner** role: an organization has one owner — see [User Roles](https://docs.typograph.nl/portal/users/roles#organization-roles) for the full model. To make someone else the owner, invite them as a member first, then [transfer ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) to them once they have joined.\n\nAn organization invite gives no access to any team or project on its own — that's granted separately, below.\n\n### Pending invitations\n\n**Pending invitations** lists everyone who was invited and has not answered yet, with their role and until when their link works. Open the actions menu (**⋮**) next to an invitation to:\n\n- **Send again**: email a new link that works for another 7 days. The link in the earlier email stops working. This is also how you renew an invitation marked **Expired**. An address gets at most one invitation mail a minute from your organization, also after you revoke its invitation, and at most 10 invitation mails a day from all organizations; you can send at most 100 a day.\n- **Revoke invitation**: withdraw it. The link stops working at once, and you can invite the person again later.\n\n[Screenshot: The Members page with three pending invitations below the members, and the actions menu of jordan.sample@example.com open with Send again and Revoke invitation]\n\nRevoking asks you to confirm first:\n\n[Screenshot: The Revoke invitation confirmation for taylor.demo@example.com, with the buttons Revoke invitation and Cancel]\n\nMembers see neither the **Invite** button nor the pending invitations.\n\n### When an invitation cannot be sent\n\nThe dialog stays open and says why:\n\n| Message | What to do |\n|---|---|\n| The address is already a member | Nothing: they are in the organization. |\n| The address has been invited already | Open the actions menu of their pending invitation and choose **Send again**. |\n| The plan has no room for more members | Members and pending invitations both count towards your plan's number of team members. Remove a member, revoke an invitation, or upgrade the plan with **See the plans**. |\n| The organization has 50 pending invitations | Revoke one, or wait until people answer. |\n| An invitation went to the address a moment ago | Wait a minute, then invite them again. |\n| As many invitations as Typograph sends in a day | The address, or you, reached the daily number of invitation mails. Try again tomorrow. |\n\n[Screenshot: The Invite someone dialog with the message that the plan of this organization has no room for more members, and a link See the plans]","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites#joining-an-organization-you-were-invited-to","section":"Joining an Organization You Were Invited To","audience":"portal","headings":["When the link does not work"],"text":"An invitation to an organization arrives by email. Open the link in it:\n\n1. If you are not signed in, sign in with the address the invitation was sent to. Without a Typograph account, choose **Sign up** and create one with that address. You come back to the invitation afterwards.\n2. Read who invited you, to which organization, and as which role.\n3. Click **Join** to become a member, or **Decline** to say no.\n\n[Screenshot: The invitation page Join Typograph Development, saying Development Typograph invited you to join as a member, with the buttons Join and Decline]\n\nAfter **Join**, the organization's Members page opens with you in it. Your own organizations stay as they are.\n\nYour open invitations also wait on your home page, under **Invitations to organizations**, with the same **Join** and **Decline** buttons:\n\n[Screenshot: The home page with the section Invitations to organizations, showing the invitation to Typograph Development with the buttons Join and Decline]\n\n### When the link does not work\n\n| You see | What it means |\n|---|---|\n| **This invitation is for another account** | You are signed in with another address than the one the invitation was sent to. Click **Switch account** and sign in with that address. |\n| **Your invitation to … expired** | The link was older than 7 days. Ask whoever invited you to send it again; you get a new link by email. |\n| **This invitation link no longer works** | The invitation was revoked, answered already, or sent again with a newer link. Look for a newer email, or ask whoever invited you. |\n\n[Screenshot: The page This invitation is for another account: it was sent to d•••@example.com while development@typograph.nl is signed in, with the buttons Switch account and Go Home]\n\n[Screenshot: The page Your invitation to Typograph Development expired, saying it expired on Oct 20 and to ask Development Typograph to send it again]\n\nWhen the organization's plan has filled up since you were invited, joining is refused and the invitation is kept: ask whoever invited you to make room, then try again.","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites#inviting-someone-to-a-team-or-project","section":"Inviting Someone to a Team or Project","audience":"portal","headings":["Editor seats"],"text":"1. Open the team or project you want to share.\n2. Click **Share** (or **Invite**).\n3. Search the people of the team by name or email address, or type an address. Each person becomes a tag; you can invite up to 20 at once.\n4. Pick a [resource role](https://docs.typograph.nl/portal/users/roles#resource-roles-team--project--template) — **Owner**, **Editor**, **Commenter**, or **Viewer**. Everyone you invite here gets the same role.\n5. Optionally, choose **+ Add a message** to send a note with the invitation.\n6. Send the invite.\n\nThe [assistant](https://docs.typograph.nl/portal/workspace/assistant#letting-the-assistant-do-it-for-you) can send the same invitations for you.\n\nA **team invite** gives access to the team and every project inside it. A **project invite** gives access to just that one project, without touching the rest of the team.\n\n### Editor seats\n\nYour plan sets how many people can edit in the organization: its **editor seats**. Everyone who can edit or manage something in the organization (**Can edit** or **Admin** on a team, project or template, or an open invitation for either) takes one seat, however many things they can edit. Everyone who can only view or comment is free, and you can invite as many of them as you like.\n\n| Plan | Editor seats |\n|---|---|\n| **Free** | 1: the owner's. Everyone you share with can view or comment. |\n| **Pro** | 5 |\n| **Enterprise** | 10 |\n\nOnce the seats are all taken, the **Share** dialog turns **Can edit** and **Admin** off, for whoever you invite and for everyone in **Who has access** who holds no seat yet. Someone who already edits or manages something else in the organization holds a seat, so for them both stay on: moving them up here takes no new seat. For everyone else they are greyed out with a lock and still say what they allow; point at one to see why you can't choose it. The line under the invite row says what you can do instead:\n\n[Screenshot: The Share dialog of Studio Noord team with alex.demo@example.com entered and the permission list open: Can view and Can comment can be chosen; Can edit and Admin are greyed out with a lock and still say what they allow, and the pointer on Can edit shows the tooltip Your plan's one editor seat is taken. Under the invite row: Everyone else you share with can view or comment. Another plan gives more editor seats, with the link See the plans]\n\n- Invite them to view or comment instead.\n- Free a seat: give someone who edits **Can view** or **Can comment**, remove them, or revoke an open invitation to edit.\n- Click **See the plans** to ask for another plan.\n\nIf the seats were taken by someone else while the dialog was open, sharing is refused with the same message, and nothing is sent.\n\nYou can see how many seats are taken on the [Members](https://docs.typograph.nl/portal/organization/members#editor-seats) page.","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites#accepting-or-ignoring-a-team-or-project-invite","section":"Accepting or Ignoring a Team or Project Invite","audience":"portal","headings":[],"text":"1. Open your notifications, or go to **My Invites**.\n2. Find the invite.\n3. Click **Accept** to add the resource to your Portal, or **Ignore** to dismiss it without granting access.\n\nAfter **Accept** on a team invite, the team opens and a message confirms that you joined it. A team of an organization you are not a member of sits in the sidebar under that organization, marked **External**; the message names where to find it (see [Teams of Another Organization](https://docs.typograph.nl/portal/getting-started/navigation#teams-of-another-organization)).\n\nOnce you've accepted or ignored an invite, it can't be re-actioned — it's a one-time decision.\n\nAn invitation to edit or manage that was sent before the organization's [editor seats](https://docs.typograph.nl/portal/organization/invites#editor-seats) were all taken cannot be accepted while they are: the page says so and the invitation stays open. Ask whoever invited you to free a seat, or to invite you to view or comment.","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Invites","url":"https://docs.typograph.nl/portal/organization/invites#related","section":"Related","audience":"portal","headings":[],"text":"- [Organization Structure](https://docs.typograph.nl/portal/organization/structure) — how organizations, teams, and projects relate\n- [User Roles](https://docs.typograph.nl/portal/users/roles) — the full organization and resource role model\n- [Permissions](https://docs.typograph.nl/portal/users/permissions) — the detailed permission model and resource-level sharing","agent_tools":["organization_invitation_create","organization_invitation_list","organization_invitation_resend","organization_invitation_revoke","organization_invitation_list_mine","organization_invitation_accept","organization_invitation_decline","invitation_create","invitation_list_resource","invitation_list_my","invitation_accept","invitation_decline","invitation_delete"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members","section":null,"audience":"portal","headings":[],"text":"The **Members** page lists everyone in your organization, with their [organization role](https://docs.typograph.nl/portal/users/roles#organization-roles) and when they joined. An owner or admin can [invite someone](https://docs.typograph.nl/portal/organization/invites#inviting-someone-to-your-organization) with **Invite**, give a member another role, or remove them from the organization.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#editor-seats","section":"Editor seats","audience":"portal","headings":[],"text":"Under **Members** the page shows how many of your plan's editor seats are taken, such as **3 of 5 editor seats**. A seat is taken by everyone who can edit or manage something in the organization; viewers and commenters are free. When they are all taken, the page says so, with a link to the plans:\n\n[Screenshot: The Members page of Studio Noord: under the heading 1 of 1 editor seat, People who can edit or manage anything in this organization. Viewers and commenters are free, and the line Your plan's one editor seat is taken. Everyone else you share with can view or comment. Another plan gives more editor seats, with the link See the plans]\n\nWhat a seat is, and what to do when they are taken: [Invites → Editor seats](https://docs.typograph.nl/portal/organization/invites#editor-seats).","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#who-can-change-members","section":"Who can change members","audience":"portal","headings":[],"text":"| You are | You can |\n|---|---|\n| **Owner** | Change the role of any other member, remove them, and [transfer ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) |\n| **Admin** | Change the role of any other member except the owner, and remove them |\n| **Member** | See who is in the organization; there are no actions for you here |\n\nNobody gets actions on their own row, and nobody gets actions on the owner's row: only a transfer changes who the owner is.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#changing-a-members-role","section":"Changing a member's role","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Members**.\n2. Open the actions menu (**⋮**) next to the member, and choose **Change role**.\n\n   [Screenshot: The Members page with the actions menu of Robin Tester open, showing Change role, Transfer ownership and Remove member]\n\n3. Pick the role. The member's role today is marked **Current role**.\n\n   [Screenshot: The Change role dialog for Robin Tester: Member is marked as the current role and Admin is selected]\n\n   | Role | What it allows |\n   |---|---|\n   | **Member** | Use the teams and projects they have been added to |\n   | **Admin** | Also manage members, apps, webhooks and settings |\n\n4. Click **Save**.\n\nThe members list reloads with the new role, and a message confirms the change.\n\nThe dialog does not offer **Owner**: an organization has one owner. To make someone else the owner, [transfer ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) to them.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#removing-a-member","section":"Removing a member","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Members**.\n2. Open the actions menu (**⋮**) next to the member, and choose **Remove member**.\n3. Read the confirmation, then click **Remove member**, or **Cancel** to keep them.\n\n   [Screenshot: The Remove member confirmation for Robin Tester, with the buttons Remove member and Cancel]\n\nThe person loses access to the organization's teams and projects straight away. Their own Typograph account stays.\n\nIf someone else removed the member after you opened the page, the list reloads and says the member was already removed.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#when-a-change-is-refused","section":"When a change is refused","audience":"portal","headings":[],"text":"Sometimes the portal cannot make the change, because something changed after you opened the page. The dialog then tells you what happened and offers one way on: **Reload the members**. Nothing has changed at that point.\n\n[Screenshot: The Change role dialog with the message that Robin Tester is no longer a member of this organization, and the button Reload the members]\n\n| Message | What happened |\n|---|---|\n| You can no longer change or remove members | Your own role changed: you are no longer an owner or admin. |\n| The member is no longer a member of this organization | Someone else removed them. |\n| This role cannot be given to the member, or the member cannot be removed | Their role changed; for example, they became the owner. |\n\nReload the members to see everyone's current role, and try again if the change still applies.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#over-the-api","section":"Over the API","audience":"portal","headings":[],"text":"Members are invited, changed and removed over the API with the same role rules: see [Identity Service → Organization members](https://docs.typograph.nl/guides/api/identity-service#organization-members) and [Organization invitations](https://docs.typograph.nl/guides/api/identity-service#organization-invitations). One difference: over the API a member can also remove themselves, which the portal does not offer. Note that the API addresses a member by the membership's `id`, not by their user ID. An [AI agent](https://docs.typograph.nl/guides/api/mcp-server#organizations) can change roles and remove members too, with the tools `organization_member_update_role` and `organization_member_remove`, when it connects through an app of your organization that holds `identity:write`.","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Managing Members","url":"https://docs.typograph.nl/portal/organization/members#related","section":"Related","audience":"portal","headings":[],"text":"- [User Roles](https://docs.typograph.nl/portal/users/roles) — what an owner, admin and member can do\n- [Invites](https://docs.typograph.nl/portal/organization/invites) — inviting someone into the organization, or to a team or project\n- [Transferring Ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) — making another member the owner","agent_tools":["organization_member_list","organization_member_update_role","organization_member_remove"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure","section":null,"audience":"portal","headings":[],"text":"Typograph organizes everything you create under a single hierarchy: **Organization → Teams → Projects**. Knowing how these three levels relate makes it much easier to find your way around sharing, roles, and billing.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#the-hierarchy","section":"The Hierarchy","audience":"portal","headings":[],"text":"```\nOrganization\n  └─ Teams\n       └─ Projects\n            ├─ Templates   (the design files)\n            ├─ Media       (images and assets used in templates)\n            └─ Fonts       (fonts used in templates)\n```\n\n- **Organization** — the top-level account. One is created for you automatically the first time you sign up, with you as its owner. You can belong to multiple organizations (for example, a personal one plus your employer's), and [create another](https://docs.typograph.nl/portal/organization/create-organization) for another company or a client.\n- **Teams** — groups of collaborators within an organization. Use teams to separate departments, clients, or workflows.\n- **Projects** — bundles of related work inside a team. Each project owns its own templates, media, and fonts — these three never span across projects.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#opening-another-organization","section":"Opening another organization","audience":"portal","headings":[],"text":"1. Open your avatar menu at the top right and choose **Organizations**.\n2. Click the organization you want to work in.\n\nIts settings, members, teams, apps, webhooks, plan and usage open, each on its own tab, and so do the [assistant's settings](https://docs.typograph.nl/portal/organization/assistant) where it is available.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#creating-a-team","section":"Creating a Team","audience":"portal","headings":["When your plan has no room for another team"],"text":"1. Go to **Organizations → {Your Org} → Teams**.\n2. Click **Create team**.\n3. Give the team a name and click **Create**.\n\n### When your plan has no room for another team\n\nYour plan sets how many teams the organization can have: **Free** has one, **Pro** and **Enterprise** have no limit (see [Subscription & Usage](https://docs.typograph.nl/portal/organization/billing#teams-and-editor-seats)). Under **Teams** the page shows how many are in use, such as **1 of 1 team**. Once they are all in use, **Create team** is off, and the page says why:\n\n[Screenshot: The Teams page of Studio Noord: 1 of 1 team under the heading, the Create team button turned off, and the line Your plan allows one team. Delete a team first, or ask about another plan, with the link See the plans]\n\nTo make room, [move a team to the trash](https://docs.typograph.nl/portal/organization/structure#renaming-a-team-or-project-or-moving-it-to-the-trash), or click **See the plans** to ask for another plan. Teams in the trash don't count, but restoring one does: a team is restored only while there is room for it.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#creating-a-project","section":"Creating a Project","audience":"portal","headings":[],"text":"1. Open the team you want the project in.\n2. Click **New project**, at the top right or on the dashed **Create new Project** tile after the projects.\n3. Give the project a name of 3 to 255 characters and click **Create project**.\n\n   [Screenshot: The New project dialog, with Spring campaign typed under Project name and the buttons Create project and Cancel]\n\nYou land in the new project, with the message **Project created**. It starts empty: add templates, media, and fonts to it once it exists.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#moving-a-project","section":"Moving a Project","audience":"portal","headings":[],"text":"Need to move a project to a different team, for example after a reorganization?\n\n1. Open the project's **⋮** menu, at the top right of the project or on its card in the team (see [below](https://docs.typograph.nl/portal/organization/structure#from-the-team-without-opening-the-project)), and select **Move**.\n2. Pick the destination team.\n3. Confirm the move on the summary screen.\n\nThe project keeps its templates, media, fonts, and sharing settings — only its parent team changes.","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#renaming-a-team-or-project-or-moving-it-to-the-trash","section":"Renaming a Team or Project, or Moving It to the Trash","audience":"portal","headings":["By its title","From the team, without opening the project"],"text":"1. Open the team or project, and open its **⋮** menu, next to **Share** at the top right.\n2. Choose **Rename** to change its display name, or **Move team to trash** / **Move project to trash** to remove it.\n\n   [Screenshot: The menu of the project Example project, with Rename, Move, Info and Move project to trash]\n\n3. To move it to the trash, type its name and click **Move to trash**. The button stays disabled until the name matches.\n\n   [Screenshot: The Move project to trash? dialog, saying the project's admins can still restore it from the trash and asking to type the project name to confirm, with the buttons Move to trash and Cancel]\n\nAfter moving a project you land on its team's projects, with the message **Project moved to trash**; after moving a team, on your dashboard, with **Team moved to trash**. Nothing is erased right away: the team or project, and everything inside it, can be restored from the trash for 30 days by its admins (for a team, the team admins). See [Trash](https://docs.typograph.nl/portal/workspace/trash) for how to bring it back.\n\n### By its title\n\nYou can also rename a team or project by clicking its name at the top of the page. A pencil shows next to the name when you point at it, and the **Rename** dialog opens. You see this only when you can edit the team or project.\n\n[Screenshot: The top of the team page Demo team, with a pencil next to the team name, and the buttons New project, Share and the ⋮ menu]\n\n### From the team, without opening the project\n\nEach project's card in the team has a **⋮** menu of its own, next to the heart: **Rename**, **Move**, **Info** (how much storage the project uses), and **Move to trash** for the team's admins. They open the same dialogs as the menu inside the project.\n\n[Screenshot: The card of the project Spring campaign, with its menu open: Rename, Move, Info and Move to trash]","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Organization Structure","url":"https://docs.typograph.nl/portal/organization/structure#related","section":"Related","audience":"portal","headings":[],"text":"- [Invites](https://docs.typograph.nl/portal/organization/invites) — how access is granted at the organization, team, and project level\n- [Portal Overview](https://docs.typograph.nl/portal/getting-started/overview) — where organization-level tools (apps, webhooks, subscription, usage) live\n- [User Roles](https://docs.typograph.nl/portal/users/roles) — the full organization and resource role model\n- [Permissions](https://docs.typograph.nl/portal/users/permissions) — the detailed permission model and resource-level sharing","agent_tools":["organization_list","team_list","team_create","team_update","team_delete","project_list","project_create","project_update","project_delete"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership","section":null,"audience":"portal","headings":[],"text":"An organization has exactly one owner. Only the owner can manage the subscription and billing, and only the owner can [delete the organization](https://docs.typograph.nl/portal/organization/delete-organization). When someone else should take that over, the owner hands the organization to another member. Ownership cannot be shared, and no admin can give it out: the owner is the only person who can transfer it.","agent_tools":["organization_transfer_ownership"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership#what-changes","section":"What changes","audience":"portal","headings":[],"text":"| | Before | After |\n|---|---|---|\n| **You** | Owner | Admin |\n| **The member you choose** | Admin or member | Owner |\n\nAs an admin, you keep managing members, apps, webhooks and settings. You can no longer:\n\n- manage the subscription and billing;\n- delete the organization;\n- make yourself the owner again. Only the new owner can transfer the organization back to you.\n\nTeams, projects, templates and everyone else's roles stay as they are.\n\nAdmins and members see under **Settings → Ownership** who the owner is, since only the owner can transfer the organization.","agent_tools":["organization_transfer_ownership"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership#transferring-the-organization-to-a-member","section":"Transferring the organization to a member","audience":"portal","headings":[],"text":"The person must already be a member of the organization. To hand it to someone outside the organization, [invite them](https://docs.typograph.nl/portal/organization/invites) first and wait until they have accepted. While nobody else is a member, **Settings → Ownership** says so instead of offering **Choose the new owner**.\n\n1. Go to **Organizations → {Your Org} → Members**. Or start from **Organizations → {Your Org} → Settings**: under **Ownership**, click **Choose the new owner**. The Members page then says how to pick the member.\n\n   [Screenshot: Organization Settings with the Ownership card: you own this organization, and the button Choose the new owner]\n\n2. Open the actions menu (**⋮**) next to the member, and choose **Transfer ownership**. Only the owner sees this item.\n\n   [Screenshot: A member's actions menu on the Members page, with Transfer ownership highlighted below Change role]\n\n3. Read the confirmation. It names the member and the organization, and lists what you lose.\n\n   [Screenshot: The Transfer ownership confirmation, listing what you can no longer do, with the button Make Sam Sample the owner and Cancel]\n\n4. Click **Make {name} the owner** to transfer the organization, or **Cancel** to leave everything as it was.\n\nThe members list reloads with the new roles, and a message confirms who the owner is now.\n\n[Screenshot: The Members page after the transfer: you are an admin, Sam Sample is the owner, and a message says Sam Sample is now the owner of the organization]","agent_tools":["organization_transfer_ownership"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership#when-the-transfer-is-refused","section":"When the transfer is refused","audience":"portal","headings":[],"text":"Sometimes the portal cannot complete the transfer. When that happens, the confirmation tells you why and offers one way forward. Nothing has changed at that point.\n\n[Screenshot: The Transfer ownership dialog with the message that Sam Sample is deleting their account and cannot become the owner, and a Close button]\n\n| Message | What happened | What to do |\n|---|---|---|\n| The member is deleting their account | Someone whose account is being deleted cannot take over the organization. | Choose another member. |\n| The member is no longer a member, or is the owner already | Someone else removed the member, or transferred the organization, after you opened the page. | Click **Reload the members** to see the current list. |\n| You are no longer the owner | Someone else became the owner after you opened the page. | Click **Reload the members** to see your current role. |\n| The organization no longer exists | The organization was deleted after you opened the page. | Click **Go to your organizations**. |","agent_tools":["organization_transfer_ownership"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership#over-the-api-and-with-an-ai-agent","section":"Over the API and with an AI agent","audience":"portal","headings":[],"text":"The transfer works the same way outside the portal:\n\n- **API:** call `POST /v1/identity/organizations/{organizationId}/members/transfer-ownership`. See [Identity Service → Transfer ownership](https://docs.typograph.nl/guides/api/identity-service#transfer-ownership).\n- **AI agent:** use the [MCP server](https://docs.typograph.nl/guides/api/mcp-server#organizations) tool `organization_transfer_ownership`.\n\nIn both cases, only the owner can transfer the organization.","agent_tools":["organization_transfer_ownership"]},{"title":"Transferring Ownership","url":"https://docs.typograph.nl/portal/organization/transfer-ownership#related","section":"Related","audience":"portal","headings":[],"text":"- [User Roles](https://docs.typograph.nl/portal/users/roles) — what an owner, admin and member can do\n- [Managing Members](https://docs.typograph.nl/portal/organization/members) — changing a member's role, or removing them\n- [Invites](https://docs.typograph.nl/portal/organization/invites) — adding the person to your organization first\n- [Billing](https://docs.typograph.nl/portal/organization/billing) — what the new owner takes over\n- [Deleting an Organization](https://docs.typograph.nl/portal/organization/delete-organization) — when the organization should go instead","agent_tools":["organization_transfer_ownership"]},{"title":"Webhooks","url":"https://docs.typograph.nl/portal/organization/webhooks","section":null,"audience":"portal","headings":[],"text":"A webhook lets Typograph notify another system the moment something happens here — a template published, a project shared, and so on — instead of that system having to keep checking for updates. You'll need at least one [App](https://docs.typograph.nl/portal/organization/apps) first, since every webhook is attached to one.","agent_tools":["webhook_list","webhook_get","webhook_create","webhook_update","webhook_delete","webhook_list_deliveries","webhook_retry_delivery","webhook_list_event_types"]},{"title":"Webhooks","url":"https://docs.typograph.nl/portal/organization/webhooks#creating-a-webhook","section":"Creating a Webhook","audience":"portal","headings":[],"text":"1. Go to **Organizations → {Your Org} → Webhooks**.\n2. Click **New Webhook**.\n3. Choose which **App** it belongs to.\n4. Give it a name.\n5. Enter the **web address** that should receive the notification whenever the event happens.\n6. Optionally, add up to 5 custom headers (`Header-Name: value`, one per line) — use this if the receiving system needs a shared secret or authentication header.\n7. Select which **events** should trigger this webhook. Only pick the ones you'll actually act on.\n8. Save.","agent_tools":["webhook_list","webhook_get","webhook_create","webhook_update","webhook_delete","webhook_list_deliveries","webhook_retry_delivery","webhook_list_event_types"]},{"title":"Webhooks","url":"https://docs.typograph.nl/portal/organization/webhooks#checking-deliveries","section":"Checking Deliveries","audience":"portal","headings":[],"text":"1. Open the webhook from your webhook list.\n2. Go to its **Deliveries** tab.\n\nHere you can see recent notification attempts — useful for confirming the receiving system is getting them, or for figuring out why it isn't.","agent_tools":["webhook_list","webhook_get","webhook_create","webhook_update","webhook_delete","webhook_list_deliveries","webhook_retry_delivery","webhook_list_event_types"]},{"title":"Webhooks","url":"https://docs.typograph.nl/portal/organization/webhooks#deleting-a-webhook","section":"Deleting a Webhook","audience":"portal","headings":[],"text":"1. Open the webhook from your webhook list.\n2. Click **Delete**.\n\nThis stops future notifications immediately. Past deliveries stay visible in the delivery history.","agent_tools":["webhook_list","webhook_get","webhook_create","webhook_update","webhook_delete","webhook_list_deliveries","webhook_retry_delivery","webhook_list_event_types"]},{"title":"Webhooks","url":"https://docs.typograph.nl/portal/organization/webhooks#related","section":"Related","audience":"portal","headings":[],"text":"- [Apps](https://docs.typograph.nl/portal/organization/apps) — creating the app a webhook attaches to\n- [API → Webhooks](https://docs.typograph.nl/guides/api/webhooks) — technical details for developers: payload shapes and the full event catalog","agent_tools":["webhook_list","webhook_get","webhook_create","webhook_update","webhook_delete","webhook_list_deliveries","webhook_retry_delivery","webhook_list_event_types"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions","section":null,"audience":"portal","headings":[],"text":"Permissions in Typograph are **resource-scoped**. Every action checks the role you hold on the specific resource you're touching — team, project, or template — not a single global role per user.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#how-permissions-are-granted","section":"How Permissions Are Granted","audience":"portal","headings":[],"text":"- **Joining an organization** gives you an **organization role** (owner, admin, member). This controls organization-wide administration only.\n- **Being invited to a team, project, or template** gives you a **resource role** (owner, editor, commenter, viewer) on that resource. It inherits to children of that resource — e.g. an editor on a project can edit templates in that project.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#permission-model","section":"Permission Model","audience":"portal","headings":[],"text":"At a glance:\n\n| Capability | Owner | Editor | Commenter | Viewer |\n|-----------|:-----:|:------:|:---------:|:------:|\n| View contents | ✓ | ✓ | ✓ | ✓ |\n| Comment | ✓ | ✓ | ✓ | |\n| Edit content | ✓ | ✓ | | |\n| Invite others | ✓ | ✓ | | |\n| Change roles | ✓ | | | |\n| Delete / move to trash | ✓ | | | |\n\n(Exact capabilities are maintained by the Identity Service and may expand over time. The Portal always reflects live configuration.)","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#organization-only-capabilities","section":"Organization-Only Capabilities","audience":"portal","headings":[],"text":"These aren't controlled by resource roles — only by organization role:\n\n| Capability | Owner | Admin | Member |\n|-----------|:-----:|:-----:|:------:|\n| View org members | ✓ | ✓ | ✓ |\n| Invite org members | ✓ | ✓ | |\n| [Change a member's role or remove them](https://docs.typograph.nl/portal/organization/members) | ✓ | ✓ (not the owner) | |\n| Manage OAuth apps | ✓ | ✓ | |\n| Manage webhooks | ✓ | ✓ | |\n| Let the [apps of guests](https://docs.typograph.nl/portal/organization/guest-apps) read, and block one | ✓ | ✓ | |\n| Change subscription | ✓ | | |\n| Delete organization | ✓ | | |","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#sharing-a-resource","section":"Sharing a Resource","audience":"portal","headings":[],"text":"1. Open the team, project, or template.\n2. Click **Invite**.\n3. Enter email, pick a role.\n4. The invitee receives an invite — once accepted, the resource shows up under **Shared with me**.\n\nOnly the address the invite was sent to can accept it. Opening the link while signed in with another account shows which address the invite went to (partly hidden, like `j•••@example.com`) and a **Switch account** button: sign in with the right account and you land back on the invite.\n\nAn invite to a team gives access to everything in that team. An invite to a specific project or template limits access to just that scope.\n\n**Can edit** and **Admin** each take one of your plan's editor seats, once per person; viewing and commenting is free. When the seats are all taken, the dialog turns both off: see [Invites → Editor seats](https://docs.typograph.nl/portal/organization/invites#editor-seats).","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#which-role-counts","section":"Which Role Counts","audience":"portal","headings":[],"text":"Someone can hold a role on a team and another on a project or template in it. The role closest to what they open counts, also when it is lower: an editor of the team can be given **Can comment** on one project, and then comments there while editing everything else in the team.\n\nAn admin is the exception: whoever is admin of a team stays admin of every project and template in it, and an admin of a project stays admin of every template in it. Giving them less there would change nothing, so Typograph refuses it:\n\n- Inviting them to a project or template with a role below **Admin** is refused, naming their address: they are admin of the team or project it is in, and stay admin here. Invite them as **Admin**, or leave their access as it is.\n- In the **Share** dialog, their row reads **Admin through the team** or **Admin through the project**, without a role to pick.\n- An invitation for less, sent before they became admin above, says so when they open it, and cannot be accepted: ignore it.\n\nAccepting an invitation for less than you can do now, as an editor of the team invited to comment on one project, is allowed: the invitation page says what you can do already, and that accepting gives you the lower role there instead.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#your-own-access","section":"Your Own Access","audience":"portal","headings":[],"text":"Every team, project and template shows your role beside its title: **Can view**, **Can comment**, **Can edit** or **Admin**. In the editor it sits in the bar above the design. A role that ends says until when: **Can view · until 12 Dec**.\n\n[Screenshot: The project Example project, with the label Can comment beside its title]\n\nClick it to see what your role means there:\n\n- **Your role**, in the words the **Share** dialog uses: **Can comment** is \"View, download and leave comments\".\n- **Where it comes from**: **Through the team Marketing**, with a link to it, when it comes with the team or project it is in. When it was given on this page itself, it says **Given to you on this project**. When your role ends, it says when, and what you keep after that.\n- **Set lower here**: if you can do more in the team or project above, but were given less here on purpose, the panel says so, and what you get back once a role that ends has ended. The role closest to what you open counts (see [Which Role Counts](https://docs.typograph.nl/portal/users/permissions#which-role-counts)).\n- **What you can do here**: six lines, from opening it to deleting it. What your role does not give you is greyed out with a minus and the role it takes, so you also see what a higher role would let you do.\n- **Need more?**: the admins here, by name, and a form to ask them for a higher role (see [Asking for Access](https://docs.typograph.nl/portal/users/permissions#asking-for-access)). Someone who shares only a project sees the admins of that project, never those of the team they are not in. An admin has nothing more to ask for, so their panel ends above it.\n\n[Screenshot: The panel of Can comment on Example project: View, download and leave comments; given on this project, with the note that higher up you have Can edit but were given less here on purpose; two of six lines ticked, the other four greyed with the role they need; under Need more?, the admins here, Limit Walk, and the form to ask them for Can edit or Admin]\n\nPress **Escape**, click the cross or click outside the panel to close it. On a phone the panel opens as a sheet at the bottom of the screen; tap above it to close it.\n\nUnder **Shared with me**, each card shows your role on it in the same words.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#asking-for-access","section":"Asking for Access","audience":"portal","headings":[],"text":"When your role is not enough, ask the admins for more from the label beside the title:\n\n1. Click your role, such as **Can view**, beside the title of the team, project or template.\n2. Under **Need more?**, pick the role you need in **Ask for**: only the roles above yours are offered, **Can comment**, **Can edit** or **Admin**.\n3. Optionally, under **Message (optional)**, tell the admins what you need it for, in up to 500 characters.\n4. Click **Ask for access**.\n\n[Screenshot: Under Need more?, the admins here: Limit Walk. Ask for: Can edit, a message about taking over the spring campaign, the note Up to 500 characters, and the button Ask for access]\n\nThe admins of the team, project or template get a notification; when it has none, the admins of the project or team it is in do. The panel now says what you asked for, such as \"You asked for Can edit. The admins have been told, and you get a notification when they answer.\" Open it later and it says when you asked.\n\nYou get a notification with the answer: the role you were given, which may differ from the one you asked for, or that your request was declined. A request nobody answers within 30 days lapses, and you are told that too; you can ask again.\n\nYou can have one request open per team, project or template. To ask for another role, click **Withdraw request** in the same panel, then ask again. You can have at most 20 requests open at once.\n\nYou can only ask where you can open the team, project or template already. To get into something you cannot see at all, ask the person who works on it to invite you.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#answering-requests-for-access","section":"Answering Requests for Access","audience":"portal","headings":[],"text":"Whoever can share a team, project or template sees the requests for it in its **Share** dialog, under **Asking for access**, above **Who has access**. The notification about a request links to the page it is about, where you open **Share**.\n\n[Screenshot: The Share dialog of Demo team: under Asking for access 1, Demo User asks for Can edit, two hours ago, with their message; beside it the role to give, Can edit, and the buttons Approve and Decline; below it Who has access]\n\nEach request shows who asks, for which role and how long ago (**asks for Can edit · 2 hours ago**), and their message, if they wrote one. To answer it:\n\n- **Approve** gives them the role in the list beside it. It starts at the role they asked for; pick another to give more or less. They get that role on this team, project or template itself, and **Who has access** shows it at once.\n- **Decline** turns the request down.\n\nEither way the request leaves the list and they get a notification.\n\nWho can answer which request:\n\n- **Can edit** answers requests for **Can comment** and **Can edit** from people who have no role on this page itself yet, such as someone who sees it through the team.\n- **Admin** answers every request, including those for **Admin** and those of people whose role was set on this page, also when it was set lower on purpose.\n\nA request you cannot answer is not listed for you. Giving **Can edit** or **Admin** takes one of your plan's editor seats, as an invite does; when they are all taken, approving says so, and you can give **Can comment** instead (see [Invites → Editor seats](https://docs.typograph.nl/portal/organization/invites#editor-seats)).","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#seeing-who-has-access","section":"Seeing Who Has Access","audience":"portal","headings":[],"text":"The **Share** dialog of a project or template lists under **Who has access** everyone who can open it. People who have access because they are in the team (for a project) or in the project and its team (for a template) are grouped in one row: **Everyone in** the team or project, **via the team** or **via the project**, with how many people that is.\n\n[Screenshot: The Share dialog of Demo project: under Who has access, the row Everyone in Demo team, via the team, 2 people, with a chevron pointing down]\n\nClick that row to see who they are. It opens to list each person, with their email address and the role they have there; click it again to close it.\n\n[Screenshot: The same dialog with the row open: under Everyone in Demo team it lists Development Typograph (you) as Admin and walk1095 as Can view]\n\nThe list is for reading only. To add someone to the team or project, or to change or remove their role, open the team or project itself and use its **Share** dialog. Someone who is in a project but not in its team sees, in a template's dialog, the project's own people and a note that others have access through the team. In the project's own dialog they see the team's row with how many people are in it; opened, it says that only people in the team can see who is in it.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#revoking-access","section":"Revoking Access","audience":"portal","headings":[],"text":"Open the resource's **Share** dialog and remove the user, or change their role. On a project page, the faces next to **Share** show the first four people with access; when there are more, the count after them (**+3**) opens the same dialog with everyone. Removal is immediate — their next request fails with a 403.","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#api-parity","section":"API Parity","audience":"portal","headings":[],"text":"The same permission checks apply over the REST API. Token scopes (`file:read`, `file:write`, `file:delete`) gate the *categories* of operations, and resource-role checks gate the *specific resources* the token can touch. See [API → Token Types](https://docs.typograph.nl/guides/api/token-types) and [API → Scopes](https://docs.typograph.nl/guides/api/scopes).","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"Permissions","url":"https://docs.typograph.nl/portal/users/permissions#related","section":"Related","audience":"portal","headings":[],"text":"- [Roles](https://docs.typograph.nl/portal/users/roles) — the role catalog\n- [API → File Service](https://docs.typograph.nl/guides/api/file-service) — invites, roles, permissions and [requests for access](https://docs.typograph.nl/guides/api/file-service#asking-for-access)","agent_tools":["permission_get","role_list","role_update","role_delete","access_request_create","access_request_list","access_request_approve","access_request_decline","access_request_withdraw"]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles","section":null,"audience":"portal","headings":[],"text":"Typograph assigns roles at two levels:\n\n1. **Organization role** — your position in an organization (member, admin, owner). Controls who can manage members, apps, webhooks, and the subscription.\n2. **Resource role** — your role on a specific team, project, or template. Controls whether you can view, edit, or delete that particular resource.\n\nThis split lets you be an organization admin overall while still only editing specific projects, or vice versa — a guest with no organization role can still be given editor rights on a single template.","agent_tools":[]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles#organization-roles","section":"Organization Roles","audience":"portal","headings":[],"text":"| Role | What they can do |\n|------|------------------|\n| **Owner** | Full control: manage members, apps, webhooks, subscription, delete the organization, [transfer ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) |\n| **Admin** | Manage members, apps, webhooks; cannot change the subscription, delete the organization or transfer ownership |\n| **Member** | Use the organization's teams and projects they've been added to; no org-level administration |","agent_tools":[]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles#resource-roles-team--project--template","section":"Resource Roles (Team / Project / Template)","audience":"portal","headings":[],"text":"These apply to a specific resource and determine what you can do with that resource's contents.\n\n| Role | View | Edit | Share / invite | Delete |\n|------|:----:|:----:|:--------------:|:------:|\n| **Owner** | ✓ | ✓ | ✓ | ✓ |\n| **Editor** | ✓ | ✓ | ✓ | |\n| **Commenter** | ✓ | | | |\n| **Viewer** | ✓ | | | |\n\n(Exact role catalog and capabilities are owned by the **Identity Service**, which is the source of truth. See [Identity Service](https://docs.typograph.nl/guides/api/identity-service) for the API-side model. Roles can be adjusted over time; the Portal always reflects the live configuration.)","agent_tools":[]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles#where-each-role-applies","section":"Where Each Role Applies","audience":"portal","headings":[],"text":"- **Organization roles** are granted when you're invited into the organization (or it's created with you as owner). An organization has one owner; the owner can [hand the organization to another member](https://docs.typograph.nl/portal/organization/transfer-ownership), and becomes an admin.\n- **Resource roles** are granted when someone invites you to a specific team, project, or template. You can have different roles on different resources.","agent_tools":[]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles#seeing-your-role","section":"Seeing Your Role","audience":"portal","headings":[],"text":"- **Your organization role** — visible in **Organizations → {Your Org} → Members**, where an owner or admin can also [change a member's role](https://docs.typograph.nl/portal/organization/members#changing-a-members-role).\n- **Your resource role** — visible in each resource's **Share** / **Invite** dialog, and in the resource card in the sidebar.","agent_tools":[]},{"title":"User Roles","url":"https://docs.typograph.nl/portal/users/roles#related","section":"Related","audience":"portal","headings":[],"text":"- [Permissions](https://docs.typograph.nl/portal/users/permissions) — the detailed permission model and resource-level sharing\n- [Managing Members](https://docs.typograph.nl/portal/organization/members) — changing a member's role, or removing them\n- [Transferring Ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) — making another member the owner\n- [API → Identity Service](https://docs.typograph.nl/guides/api/identity-service) — org and member management over the API","agent_tools":[]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#updating-your-profile","section":"Updating Your Profile","audience":"portal","headings":[],"text":"1. Click your avatar in the top-right, then **Profile**.\n\n   [Screenshot: The profile dropdown, with Profile highlighted]\n\n2. Update any of the following:\n\n   | Field | What it's for |\n   |-------|----------------|\n   | **First name / Last name** | Shown wherever you're listed as a collaborator — member lists, share dialogs, comments |\n   | **Phone** | Optional contact number |\n\n   [Screenshot: The Personal Information form, with name, email, and phone fields]\n\n   Your **email** is shown greyed out: it is the address you sign in with and where notifications are sent. To change it, see [Changing Your Email Address](https://docs.typograph.nl/portal/workspace/account#changing-your-email-address).\n\n3. Save your changes.\n\n   [Screenshot: The Save button on the Personal Information form]","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#changing-your-email-address","section":"Changing Your Email Address","audience":"portal","headings":["Didn't change it yourself?","Limits"],"text":"You sign in with your email address, and your sign-in codes and notifications go there. To move your account to another address, you confirm twice: once with a code to your current address, and once with a code to the new one. Your organizations, projects and templates stay as they are, and invitations to your old address move to the new one. An organization's own contact and billing addresses are set in the organization, not here.\n\n1. Click your avatar in the top-right, then **Profile**. Next to **Email address**, click **Change**.\n\n   [Screenshot: The Email address field on the profile, with the Change link and the help text below it]\n\n2. Read what happens, then click **Email me a code**. Nothing changes yet.\n\n   [Screenshot: The Change your email address page, listing what happens and the Email me a code button]\n\n3. Enter the 6-digit code we sent to your current address, and your new address. Then click **Send a code to the new address**. The code works for 15 minutes. No email? Click **Send a new code**: the new code replaces the one before it.\n\n   [Screenshot: The code from the current address and the new email address filled in, with the Send a code to the new address button]\n\n4. Enter the 6-digit code we sent to your new address, then click **Change my email address**. This code also works for 15 minutes. Below the code field, you can send it again, use another address, or cancel the change. No email after a few minutes? Check the address and your spam folder. An address that already belongs to another Typograph account gets no code.\n\n   [Screenshot: The code field for the new address, with the Change my email address button and the links to send a new code, use another address, or cancel the change]\n\n5. The profile shows your new address. Sign in with it from now on. You stay signed in where you made the change; on your other devices, and in apps and agents you connected, you sign in again.\n\n   [Screenshot: The message on the profile that the email address is now the new one]\n\nLeft before entering the second code? The profile reminds you while the change waits for its code. Click **Enter the code** to continue.\n\n[Screenshot: The reminder on the profile that a code was sent to the new address, with the Enter the code button]\n\n### Didn't change it yourself?\n\nYour old address gets an email about the change, with an **Undo the change** link that works for 7 days. The link opens a page that explains what undoing does. Click **Undo and secure my account**: your account gets its old address back, and everyone is signed out, including whoever made the change. That person needed a code from your mailbox, so change your mailbox's password too. Made the change yourself? Click **I made this change myself, keep it**, or simply ignore the email. While the link works, nobody can create an account with your old address, and [signing in with it](https://docs.typograph.nl/portal/getting-started/login#when-your-address-moved-to-another-typograph-account) says that it moved.\n\n[Screenshot: The Undo the change of your email address page, with the Undo and secure my account button and the I made this change myself, keep it link]\n\n### Limits\n\n- After a change, or after undoing one, the address can change again after 30 days. Until then, the page says from which day. Need it sooner, for example because you lost access to your mailbox? [Contact support](https://docs.typograph.nl/guides/support).\n- You can ask for a code to a new address at most once a minute and 3 times a day.\n- After 5 wrong codes for your current address, wait 15 minutes. After 5 wrong codes for your new address, the change is cancelled: start again.","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#changing-your-preferences","section":"Changing Your Preferences","audience":"portal","headings":["Your Time Zone"],"text":"1. Click your avatar in the top-right, then **Profile**.\n\n   [Screenshot: The profile dropdown, with Profile highlighted]\n\n2. In the bottom-left corner, adjust the following settings:\n\n   | Setting | What it does |\n   |---------|----------------|\n   | **Email notifications** | Turns notification emails on or off, on top of the in-app [notifications](https://docs.typograph.nl/portal/workspace/notifications) |\n   | **Theme** | **Light**, **Dark** or **System** for the Portal, the editor and the sign-in pages. **System** follows the setting of your computer or browser, and switches along when that changes, for example when your computer turns dark in the evening. A new account starts on **System** |\n   | **Language** | English or Nederlands — sets the language for the whole Portal and your Typograph emails. A new account starts in the language you [signed up in](https://docs.typograph.nl/portal/getting-started/login#choosing-your-language) |\n   | **Time zone** | **System**, or a time zone you choose: the zone every time in the Portal and in your Typograph emails is shown in. See [Your Time Zone](https://docs.typograph.nl/portal/workspace/account#your-time-zone) |\n\n   [Screenshot: The Settings section: the email notifications switch, the Theme choice with Light, Dark and System, and System selected, and the Language field]\n\n3. Save your changes.\n\n   [Screenshot: The Save button below the Settings section]\n\n   The Portal switches to the theme you chose immediately after saving, here **Dark**:\n\n   [Screenshot: The Settings section in the dark theme, with Dark selected under Theme]\n\nChanges here only affect the Portal and how others see you — they don't affect which organizations, teams, or projects you have access to. That's managed through [invites](https://docs.typograph.nl/portal/organization/invites) instead.\n\n### Your Time Zone\n\nEvery time the Portal shows you, such as when a template was changed, when a notification came in, or when the [assistant's](https://docs.typograph.nl/portal/workspace/assistant#the-chip) credits start again, is in your time zone. So are the times in your Typograph emails.\n\n- **System**, the default, follows your browser: the setting shows which zone that is now, with its time. On a laptop you take abroad, the Portal follows along.\n- **Choose a time zone** sets one zone, such as **Europe/London**, for when your device's zone is not the one you work in.\n\nA day without a time, such as the day a deleted item goes for good, stays that day in every zone. Your emails use the zone you chose; with **System**, the zone your browser last reported, or else Amsterdam time.","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#usage-analytics","section":"Usage Analytics","audience":"portal","headings":["Changing your answer"],"text":"Typograph measures how the Portal is used, to see where it can be clearer. Until you say yes, that is anonymous counting only: nothing is linked to you and no session is recorded. The details are in the [privacy policy](https://typograph.nl/privacy).\n\nThe first time you open the Portal's home page, a dialog asks whether we may see how you use Typograph.\n\n[Screenshot: The dialog Help us make Typograph better, with a Yes, help improve Typograph button and a No thanks link]\n\n- **Yes, help improve Typograph** records your sessions in the Portal, the editor and forms included, linked to your user ID and your organization, never to your name or email address, and keeps an identifier in your browser. The fields you fill in in the Portal itself are never recorded. The editor and forms are recorded as you see them, with what you type there and the content of your templates.\n- **No thanks** keeps it anonymous: we only count which pages you visit and what you click or open, and store nothing on your device but your answer.\n\nYour answer is kept with your account, so it follows you to every browser and the dialog does not ask again. Not ready to decide? Press Esc: nothing changes, and the dialog asks again the next time you open the home page.\n\n### Changing your answer\n\n1. Click your avatar in the top-right, then **Profile**.\n\n   [Screenshot: The profile dropdown, with Profile highlighted]\n\n2. Under **Usage analytics**, turn **Allow usage analytics** on or off. The switch saves on its own, straight away: you stay on the page, a message confirms it, and anything you changed in the form above it is still there to save.\n\n   [Screenshot: The Usage analytics section of the profile, with the Allow usage analytics switch turned on]","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#deleting-your-account","section":"Deleting Your Account","audience":"portal","headings":["You own an organization with other members","Changed your mind?"],"text":"Deleting your account removes everything that is only yours: your profile, and the organizations you are the only member of, with everything in them. Organizations you share with others stay, without you. You confirm with a code we email you, and you can change your mind for 30 days.\n\n1. Click your avatar in the top-right, then **Profile**. At the bottom of the page, click **Delete account…**.\n\n   [Screenshot: The Delete account section at the bottom of the profile page, with the Delete account button]\n\n2. Read what happens, then click **Email me a code**. Nothing is deleted yet.\n\n   [Screenshot: The Delete account page, listing what happens and the Email me a code button]\n\n3. Enter the 6-digit code from the email, then click **Delete my account**. The code works for 15 minutes. No email? Click **Send a new code**: the new code replaces the one before it. Below the code you can tell us why you are leaving by picking a reason; that is optional, and **I'd rather not say** is chosen until you pick another.\n\n   [Screenshot: The code field, the optional reasons for leaving, and the Delete my account and Cancel buttons]\n\n4. You are signed out everywhere, including apps you connected, and the page shows the day your account goes for good.\n\n   [Screenshot: The Your account is being deleted page, with the day of deletion and a Sign in to keep my account button]\n\nWe email you a reminder 7 days and 1 day before that day.\n\n### You own an organization with other members\n\nAn organization with other members cannot be left without an owner. If you own one, the page lists it instead of offering a code. Use **Manage members** to remove the other members, or to make one of them the owner (see [Transferring Ownership](https://docs.typograph.nl/portal/organization/transfer-ownership)). Then come back to delete your account.\n\n[Screenshot: The Delete account page listing an organization you own, with a Manage members button]\n\n### Changed your mind?\n\nSign in within the 30 days, with the same email address. Typograph asks whether you want to keep your account. Click **Keep my account** and you carry on where you left off.\n\n[Screenshot: The Keep your account screen, with the day of deletion and the Keep my account button]\n\nIf you do nothing, the account is deleted on the day shown, and it cannot be brought back after that.","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Account Settings","url":"https://docs.typograph.nl/portal/workspace/account#related","section":"Related","audience":"portal","headings":[],"text":"- [Invites](https://docs.typograph.nl/portal/organization/invites) — how you get access to organizations, teams, and projects\n- [Signing In](https://docs.typograph.nl/portal/getting-started/login) — the login methods your account can use","agent_tools":["identity_whoami","identity_profile_update","identity_account_deletion_code","identity_account_delete"]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant","section":null,"audience":"portal","headings":[],"text":"The assistant answers questions about Typograph in your own words, finds your templates, projects and teams, and, when you let it, makes changes for you. It works for you, with your own rights, in every organization you belong to, as a member or as a guest, and it shows a plan before it changes anything. You never pick an organization to start: it follows from the work.\n\nThe assistant is in beta, and open to some organizations only. If you don't see the **Assistant** button, it isn't available to you yet.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#opening-the-assistant","section":"Opening the Assistant","audience":"portal","headings":[],"text":"Click **Assistant** next to **Search** at the top of the Portal, or press <kbd>Ctrl</kbd>+<kbd>J</kbd> (<kbd>⌘</kbd>+<kbd>J</kbd> on a Mac).\n\n[Screenshot: The header of the Portal with the Assistant button to the right of the Search field]\n\nThe assistant opens as a panel on the right. On a wide screen the page moves aside, so you can keep working next to it; on a phone it fills the screen. The panel stays open, with the same conversation, when you go to another page. Press <kbd>Esc</kbd> or click **×** to close it.\n\nThe top line of the panel holds the [credits chip](https://docs.typograph.nl/portal/workspace/assistant#who-pays-a-turn), which names the organization whose AI credits this turn uses, and the panel's buttons: **Conversations** (the clock), **+** for a new conversation, the **⋮** menu and **×**. Under the field at the bottom are the [Act for me](https://docs.typograph.nl/portal/workspace/assistant#the-act-for-me-switch) switch and **Beta · AI can make mistakes**, which links to [The AI Behind the Assistant](https://docs.typograph.nl/portal/workspace/assistant#the-ai-behind-the-assistant).","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#the-first-time","section":"The First Time","audience":"portal","headings":[],"text":"The first time you open the assistant, one screen says what it does: it sees what you see in the portal, in all your organizations; it does only what you may, always after a plan you confirm; and you turn off **Act for me** to have it only answer. Click **Start**.\n\nThere is nothing to allow or choose, and no question per organization: the assistant is part of the portal and works with your own rights. What it may do in an organization is decided by that organization's owners and admins (see [What It Can Reach](https://docs.typograph.nl/portal/workspace/assistant#what-it-can-reach)).","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#what-it-can-reach","section":"What It Can Reach","audience":"portal","headings":[],"text":"The assistant sees what you see in the portal: the teams, projects, templates, media, fonts and trash of every organization you are a member of, and of every organization where you are a guest, someone with a role on a team, project or template there. Ask *\"What is in the trash?\"* and it looks in all of them, as the portal's [trash](https://docs.typograph.nl/portal/workspace/trash) does.\n\nWhen you belong to more than one organization, every item it lists names its organization, and an organization where you are a guest carries the sidebar's **External** label.\n\nEach organization's owners and admins decide what the assistant may do there, for everyone who works there ([AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant)):\n\n- **Read and edit**, the default: it reads, and changes things after a plan you confirm.\n- **Read only**: it reads. Ask it to change something there and it says that you make that change in the portal yourself, without proposing a plan.\n- **Off**: it reads and changes nothing there.\n\nThe assistant never skips an organization in silence. When it searched your organizations and left one out, because the assistant is off there or not open to it yet, a line under the answer names it, such as *The assistant is off in Press Room. I did not search there; see the trash in the portal.*\n\nWhen owners or admins change this setting, it takes effect within a minute. A plan you confirm is checked again when it runs: if the organization has meanwhile become read only, the plan stops with that explanation, and nothing is changed.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#who-pays-a-turn","section":"Who Pays a Turn","audience":"portal","headings":["The Chip","When the Paying Organization Changes"],"text":"Every question and the work behind it, a **turn**, is paid in [AI credits](https://docs.typograph.nl/portal/workspace/assistant#ai-credits) by one organization. The chip at the top of the panel names it. The assistant picks it when you send your question:\n\n1. **The organization you picked in the chip**, if it can pay for this work.\n2. Otherwise **the organization of the work**: the team, project or template open on the page, when you are an editor or admin there.\n3. Otherwise **an organization of your own**: of the organizations where you are a member and an editor or admin, the one with the most credits left this month. Every person has one, since signing up creates it.\n\nAn organization pays only for work in itself. **Viewers and commenters never spend an organization's credits**: as a viewer somewhere, your own organization pays, and the assistant can't change anything there for you anyway. As a **guest**, a host organization pays only when its owners and admins let guests use its credits for work there, which is off by default; otherwise your own organization pays.\n\n### The Chip\n\nThe chip shows the paying organization and a bar with how much of its credits for **today** are used, in percent, turning orange from 80%.\n\n- **Point at the bar, or tab to it**, to see the details of the paying organization: what is left today and when the day starts again, and for an organization you are a member of also its plan and what is left this month. The reset times are in [your time zone](https://docs.typograph.nl/portal/workspace/account#your-time-zone), such as *Resets tomorrow at 01:00*.\n- **Click the chip** to open **Your organizations**: every organization you belong to, each with its own day bar, and a check at the one paying. You can pick the organization of the work and your own organizations. The others are greyed out with the reason: *You are a viewer there*, *The assistant is off there*, *The assistant is not open there yet*, *Guests use their own credits there*, or *Only for work in this organization*. On a phone, the list is also where you find the details.\n\nAs a guest, you see a host organization's day bar, not its plan or its month.\n\n### When the Paying Organization Changes\n\nA plan names the organization its changes land in, and which organization pays (*Credits from …*). When you run a plan in another organization than the one paying, and you may pay with that organization, it pays from then on: a quiet line in the chat says *Credits now from …*, and the chip follows. No dialog asks you first: the plan you confirmed said so.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#asking-a-question","section":"Asking a Question","audience":"portal","headings":["Long Conversations","Short Code Examples Only","Pasting a Token or Key"],"text":"Type your question in the field at the bottom and press <kbd>Enter</kbd>, or click one of the suggestions. Ask in your own language.\n\nTo ask an earlier question again, press <kbd>↑</kbd> in the empty field: your questions come back one by one, the newest first, to send as they are or change first. <kbd>↓</kbd> goes back to newer ones and then to what you were typing. The assistant remembers the questions you sent in this browser tab, in every conversation, until you close the tab.\n\nFor a *how do I…* question, the assistant searches this documentation and answers with the steps. The pages it used appear under the answer as cards; click one to read the whole page. When it can do the task for you, it offers to: click **Yes** to let it, or **No, thanks**.\n\n[Screenshot: The answer to How do I invite someone to a team?, with the steps, three documentation cards from the Invites page and the offer Yes: Sending an invitation]\n\nAn answer can hold a table, to compare a few things side by side, and links to a section of this documentation, which open in a new tab. Ask how to build something, such as adding the form widget to your website, and it shows the code from the documentation in a code block. A wide table or a long line of code scrolls sideways. Click the copy button above a code block, or tab to it and press <kbd>Enter</kbd>, to copy the code; a check confirms it.\n\n[Screenshot: The answer to How do I add the form widget to my own website?, with a link to the Form Integration guide and the HTML code in a code block, labelled html, with a copy button]\n\nIt also finds your work: *Which templates did I change this week?* lists the templates as cards that open them. Every step it takes shows as a short line, such as **Searching the documentation** or **Listing teams**.\n\nAsk who is in your organization, or who has access to a team, project or template, and the people appear as cards too: their picture, or their initial when they have none, their name, their e-mail address and their role. A role on a team, project or template reads as in the **Share** dialog (**Can view**, **Can comment**, **Can edit** or **Admin**, see [Your Own Access](https://docs.typograph.nl/portal/users/permissions#your-own-access)), a role in the organization as on its **Members** page (**Owner**, **Admin** or **Member**, see [User Roles](https://docs.typograph.nl/portal/users/roles#organization-roles)). Someone who has not accepted an invitation yet is marked **Invited**. To change who has access, open the **Share** dialog, or ask the assistant to invite someone.\n\nTo see what a step did, point at its line, tab to it, or tap it on a phone. A card opens with what that kind of step does and what this one did: what it searched for and where, whether it looked in the trash too, the name it gave something, and how many results it found. A step that did not work says so, and a step that changed something says it ran only once you said so. Press <kbd>Esc</kbd>, or tap elsewhere, to close it.\n\n[Screenshot: The step Searching under a question, with its card open: it searches the names of your templates, projects, teams, media and fonts, only what you have access to; it searched for Welcome in templates, with the trash too, and found nothing]\n\nRate an answer with the thumbs under it. **Conversations** (the clock) lists your earlier conversations; **+** starts a new one.\n\nA line above your first question shows when the conversation started, such as **Today 2:05 PM**. When you go on with it on a later day, a new line marks that day: **Yesterday**, the day of the week within the last week, or else the date. To see when a question or an answer was sent, point at it: its time appears beside the question, or after the thumbs of the answer. Point at the time for the full date. With the keyboard, tab into the conversation and every time shows; on a phone the times always show.\n\n[Screenshot: A conversation held on two days: the line Yesterday 1:05 PM above the first question and its answer, then the line Today 4:06 PM above the second question, with its time 4:06 PM beside it]\n\n### Long Conversations\n\nWith every question, the assistant reads the conversation back: the last ten questions and their answers, or fewer when they were long. Beyond that it no longer takes the oldest messages into account, so it may forget what you said at the start. The first time that happens in a conversation, a line says so, with **New conversation**. For a new topic, start a new conversation: the assistant then has nothing old in the way, and it reads back less, which costs fewer [AI credits](https://docs.typograph.nl/portal/workspace/assistant#ai-credits).\n\n### Short Code Examples Only\n\nThe assistant writes short code for working with Typograph: an API call with the client-credentials flow, a webhook receiver that checks the signature, embedding a widget, or the settings to connect your own AI assistant over MCP. It bases the code on this documentation and links the page it comes from. It doesn't write complete programs or client libraries, general programming help, or code that has nothing to do with Typograph.\n\nWhen the code in an answer gets longer than such an example, the answer stops there. The code so far stays, with its copy button, and a line under it points to the API reference.\n\n[Screenshot: An answer with the PHP receiver from the Webhooks guide, its code block stopped after a few lines, three documentation cards, and under them the yellow line I write short examples only. The rest is in the API reference, with a link]\n\nFor a complete integration, start from the [API reference](https://docs.typograph.nl/api-reference), or let your own AI assistant build it with the [MCP server](https://docs.typograph.nl/guides/api/mcp-server).\n\n### Pasting a Token or Key\n\nDon't paste passwords, access tokens, client secrets or other keys into a question. If you do, the assistant masks anything shaped like a Typograph access token, refresh token or client secret before it reads your question, and a line under your question says so. Not every key has a shape it can recognise, so don't rely on it. The masked part isn't saved with the conversation either: in your list of conversations it shows as `[masked]`. Code the assistant writes never holds a real key, only a placeholder such as `YOUR_CLIENT_SECRET`.\n\n[Screenshot: The question The API answers 401 to my token, followed by a made-up token, and under it the line Something in your question looked like a password or a key, so the assistant did not see it. The answer starts with I can't see your token]\n\nIf you did paste a real key, treat it as exposed: revoke it and create a new one.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#letting-the-assistant-do-it-for-you","section":"Letting the Assistant Do It for You","audience":"portal","headings":["A Plan in Another Organization"],"text":"When the assistant needs details, it asks for them in a small form in the chat instead of a string of questions. Choices come from your own work, such as your teams.\n\nTo invite people, the form works like the [share dialog](https://docs.typograph.nl/portal/organization/invites#inviting-someone-to-a-team-or-project): search the people of the team by name or email address, or type an address, and each one becomes a tag. You can invite up to 20 people at once with the same role, and add a message for all of them with **+ Message (optional)**. An address that isn't a valid email address stays in the field with a note, so you can correct it.\n\n[Screenshot: The form Invite colleagues to Marketing in the chat, with designer@example.com and copywriter@example.com as tags in the Email addresses field, the search suggesting Development Typograph with its email address, the role View, the link + Message (optional), and the buttons Next and Cancel]\n\nBefore it changes anything, the assistant shows a **plan**: what it will do, the team and organization every change lands in, and at its foot which organization's credits it uses (*Credits from …*). A plan changes things in one organization. Nothing happens until you choose:\n\n- **Run** carries out the plan.\n- **Adjust** leaves it unrun. A plan that came from a form opens that form again below it, filled in with what you gave, the tags and the message included: change what you want and click **Next** for a new plan. For any other plan, say in the field below what to change, and the assistant shows a new plan.\n- **Cancel** drops it. Nothing is changed.\n\n[Screenshot: The filled-in form Invite people to Demo team, and under it the plan Invite designer@example.com, copywriter@example.com to the team Demo team as Commenter, its three steps with the message, and the buttons Run, Adjust and Cancel]\n\n[Screenshot: After Adjust: the plan marked Not run: change the form below, and under it the form again with designer@example.com and copywriter@example.com as tags, the role Comment and the message Welcome to the team!, with the buttons Next and Cancel]\n\n### A Plan in Another Organization\n\nThe organizations in a plan come from the items themselves, never from the text the AI wrote. When a change lands in another organization than the one the conversation's work started in, an orange line on the plan says so: *This change lands in Example Print Co., not in the organization this turn started in.* Check it before you click **Run**: a text in one organization, such as a template or file name, could try to steer the assistant into changing another. The same goes for a step marked because it invites an address from outside the organization, sends an invitation message, or uses a name you did not type yourself.\n\nAfter the plan has run, the assistant says it is done. Where the change can be taken back, the plan shows **Undo**. Undoing an invitation to several people revokes every invitation it sent.\n\n[Screenshot: The assistant says it invited designer@example.com to Demo team, and the plan under it is marked done in green, with an Undo button]\n\nWhen your organization's plan allows no more of something, the assistant says so before it asks for any details. On Free, which allows one team, asking it for a new team shows no form: it checks your plan's limits, says that the plan allows no more teams, and offers **View usage**, where you can request another plan. Or move a team to the trash first: a team in the trash doesn't count. Inviting someone to your organization works the same way when your plan's members are all there: an open invitation holds a place too, so you can also revoke one, or remove a member, first.\n\nEditor seats are different: someone who already edits anywhere in your organization keeps their seat, so inviting them with edit rights always works. When all your plan's seats are taken, the assistant says so, and a plan to invite with edit rights carries a line under it that says the same. Anyone in it without a seat is refused edit rights when the plan runs, and the plan says why. Inviting to view or comment always works. Projects and templates have no limit on any plan.\n\nA plan that doesn't go through stops with why, such as a limit of your plan, or a role that doesn't allow the change, and nothing is changed. The step's line says the same when you point at it.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#the-act-for-me-switch","section":"The Act for Me Switch","audience":"portal","headings":[],"text":"**Act for me**, under the field, is your one switch. Off, the assistant only explains and finds things, and changes nothing, in any organization. Ask it to change something and it says that Act for me is off, gives the steps to do it yourself, and offers to do it once you switch it on.\n\n[Screenshot: With Act for me switched off, asked to create a project called Brochures, the assistant says Act for me is off and it can do it once you switch it on, then gives the steps to do it yourself, with the documentation it used under them]\n\nThe switch is on for everyone who starts with the assistant, and it remembers your choice. If you used the assistant before it worked across organizations, the switch starts where you left it: off if you had it only look along in any organization, had taken back its permission, or had never given one. On, the assistant still does only what you may do, and only where the organization's setting is **Read and edit**.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#what-the-assistant-never-does","section":"What the Assistant Never Does","audience":"portal","headings":[],"text":"- **Anything you can't do yourself.** It works with your rights in each organization, and never touches what a template locks. Where an organization lets it only read, or turned it off, it changes nothing there.\n- **A change without a plan.** Every change is shown first and waits for **Run**. Deleting, inviting and anything that costs credits always need your go-ahead.\n- **What stays with people.** Signing in and security, your account (such as deleting it), the subscription, and creating or changing apps. Ask for one of these and it says where you do it yourself.\n- **Changing your organization's members or settings.** It invites people to an organization and revokes an invitation when you are an owner or admin there, but changing a member's role, removing a member and changing the organization's details you do yourself, on the organization's [Members](https://docs.typograph.nl/portal/organization/members) and Settings pages.\n\n[Screenshot: Asked to delete the account, the assistant answers that it cannot do that for you, and gives the steps to do it yourself from your profile]\n\nThe assistant is an AI: its answers can contain mistakes. What it wants to change, you always see first. How to check an answer, and what to do when one is wrong: [When the Assistant Is Wrong](https://docs.typograph.nl/portal/workspace/assistant-mistakes).","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#ai-credits","section":"AI Credits","audience":"portal","headings":["Credits per Plan","At Most a Quarter per Day","When Credits Run Low","When Credits Are Used Up","Requesting Extra Credits","Speed Limits","When the Assistant Is Unavailable"],"text":"What the assistant does is paid with **AI credits**, a monthly allowance that comes with an organization's plan. Each turn uses the credits of one organization, as [Who Pays a Turn](https://docs.typograph.nl/portal/workspace/assistant#who-pays-a-turn) explains; the chip at the top of the panel names it and shows how much of its day is used. The organization's [usage page](https://docs.typograph.nl/portal/organization/billing#checking-your-usage) shows how many it used this month, and who used them. There is no limit per person: someone alone in an organization can use its whole day.\n\nEvery answer costs credits. How many depends on the work behind it: a short answer from what the assistant knows costs little, and a question for which it searches the documentation or your work, or makes a plan, costs more. A longer answer costs more too. Follow-up questions in the same conversation are cheaper, because the assistant reuses its instructions instead of reading them again for every question. A question that isn't answered because of a limit below costs nothing.\n\n### Credits per Plan\n\n| Plan | AI credits per month | Extra credits |\n|---|---|---|\n| **Free** | 500 | No: upgrade the plan |\n| **Pro** | 5,000 | Credit bundles, on request |\n| **Enterprise** | 20,000 | Credit bundles, on request |\n\nEvery plan gets the whole assistant, acting for you included; the plans differ only in how many credits they have. The credits start again on the first day of every month. Credits left at the end of a month don't carry over.\n\n### At Most a Quarter per Day\n\nAn organization can use **at most a quarter of its month's credits in one day**: 125 on Free, 1,250 on Pro and 5,000 on Enterprise. So one busy day, or one runaway conversation, never uses up the whole month. Credit bundles count as well: a bundle added to the month raises the day's limit at once. The day starts at midnight UTC; the chip's details say when that is in [your time zone](https://docs.typograph.nl/portal/workspace/account#your-time-zone), such as *Resets tomorrow at 01:00*.\n\n### When Credits Run Low\n\nOnce the paying organization has used 80% of the month's credits, a line in the conversation says how much is left and when new credits come. The assistant goes on answering. It says so once per conversation. Its owners and admins also get an email, once a month, that names who used the most, so colleagues can agree who does what before the credits run out.\n\n### When Credits Are Used Up\n\nWhen the paying organization's credits for the day or the month are used up, the assistant stops answering, and a message above the field says which it is and when you can go on: when none of the organizations you may pay with has credits left, the earliest moment one of them starts again. The chip shows it too. When another organization of yours may pay for this work, the message also offers **Use another organization's credits**, which opens the [chip's](https://docs.typograph.nl/portal/workspace/assistant#the-chip) list to pick it. A question that was refused stays in the conversation, marked **Not answered**, and goes back into the field, so you can send it again later. What you can do depends on your role in the paying organization and its plan:\n\n- **For the day**: you can go on tomorrow. An owner or admin on Pro or Enterprise can also [request extra credits](https://docs.typograph.nl/portal/workspace/assistant#requesting-extra-credits), and on Free choose **Upgrade plan**. Anyone else sees **View usage**.\n- **For the month**: new credits come on the first of the next month. An owner or admin on Pro or Enterprise sees **Request credits**, and on Free **Upgrade plan**, which opens the plan comparison. Anyone else is asked to turn to an owner or admin of that organization, and sees **View usage**.\n\nWhen credits are available again, the message is gone and you can ask your question.\n\n### Requesting Extra Credits\n\nOn Pro and Enterprise, an owner or admin can add a **credit bundle** to the month:\n\n| Bundle | Price |\n|---|---|\n| 5,000 AI credits | €39 |\n| 15,000 AI credits | €90 |\n\nA bundle counts for the month it is added in, on top of the plan's credits, and raises the day's limit at once. Credits of a bundle that are left at the end of the month don't carry over.\n\nThe portal doesn't sell bundles itself yet: you request one from us by email.\n\n1. Click **Request credits** in the message at a credits limit. Your email program opens a message to sales@typograph.nl with your organization's ID and both bundles filled in.\n2. Delete the bundle you don't want, change **How many** if you want more than one, and send the message.\n\nWe add the bundle to your organization. Without the button at hand, email sales@typograph.nl yourself and name your organization. On Free there are no bundles: [change the plan](https://docs.typograph.nl/portal/organization/billing#changing-your-plan) instead.\n\n### Speed Limits\n\nApart from credits, the assistant has two speed limits per person. Someone working normally never meets them; they stop a script or a hijacked account from using up the organization's credits in a few minutes. Your question stays in the field while you wait.\n\n- **One question at a time.** While the assistant is still answering a question of yours in another conversation, also in another browser tab, a new question waits. A message says so, and the question is sent by itself as soon as the other one is done. Click **Don't send** to keep it in the field instead. A plan or form that waits for you doesn't hold up a new question.\n\n  [Screenshot: The message The assistant is still answering your question in another conversation. Yours is sent as soon as it is done, with a Don't send button, and the question How do I invite someone to a team? still in the field]\n\n- **A pause after many questions in a short time.** The message says when you can go on and counts down. Then **Send** works again by itself.\n\n  [Screenshot: The message You asked a lot of questions in a short time. You can go on in 4 minutes, above the field with the question still in it and the Send button greyed out]\n\n### When the Assistant Is Unavailable\n\nRarely, the assistant pauses for everyone for a while. The message says it isn't because of you, and your question goes back into the field. Try again later; there is no countdown, and no credits are spent.\n\n[Screenshot: The question Which templates did I change this week? with the yellow line The assistant is unavailable for a while. This is not because of you. Your question is back in the field: try again later, and the question back in the field]","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#the-ai-behind-the-assistant","section":"The AI Behind the Assistant","audience":"portal","headings":[],"text":"The assistant is in beta. Its AI is **Anthropic · Claude**: what the assistant reads and writes for you is processed there. Each organization's owners and admins see this in their [AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant#ai-provider), with how we process data, and decide whether the assistant may read their organization at all.","agent_tools":[]},{"title":"Assistant","url":"https://docs.typograph.nl/portal/workspace/assistant#related","section":"Related","audience":"portal","headings":[],"text":"- [When the Assistant Is Wrong](https://docs.typograph.nl/portal/workspace/assistant-mistakes) — why an answer can be wrong, and what to do about it\n- [Search](https://docs.typograph.nl/portal/workspace/search) — find templates, projects, teams, media and fonts by name\n- [AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant) — what the assistant may do in an organization, and whether guests may use its credits\n- [Invites](https://docs.typograph.nl/portal/organization/invites) — inviting people yourself\n- [MCP server](https://docs.typograph.nl/guides/api/mcp-server) — connect your own AI assistant, such as Claude, to Typograph","agent_tools":[]},{"title":"When the Assistant Is Wrong","url":"https://docs.typograph.nl/portal/workspace/assistant-mistakes","section":null,"audience":"portal","headings":[],"text":"The [assistant](https://docs.typograph.nl/portal/workspace/assistant) is an AI. It writes its answers itself, from Typograph's documentation and from what it finds in your organizations. Most answers are right, but an answer can be wrong, incomplete or out of date, even when it sounds sure of itself. That is why the panel says **AI can make mistakes** under the field.","agent_tools":[]},{"title":"When the Assistant Is Wrong","url":"https://docs.typograph.nl/portal/workspace/assistant-mistakes#why-an-answer-can-be-wrong","section":"Why an Answer Can Be Wrong","audience":"portal","headings":[],"text":"- **It predicts text.** The assistant puts an answer together from patterns in language. It does not know whether a sentence is true, so it can state something that sounds right and isn't.\n- **It can misread your question.** A short or ambiguous question may be answered as a different question than the one you meant.\n- **It only sees what it looks up.** For a *how do I…* question it searches this documentation; for your work it looks at what your rights let it see. What it did not look up, it can only guess.\n- **Things change.** A page, a template or a team may have changed since the assistant read it in this conversation.","agent_tools":[]},{"title":"When the Assistant Is Wrong","url":"https://docs.typograph.nl/portal/workspace/assistant-mistakes#how-to-check-an-answer","section":"How to Check an Answer","audience":"portal","headings":[],"text":"- **Open the sources.** Under an answer that used the documentation, the pages it read show as cards. Click one to read the whole page and compare.\n- **Look at the cards it found.** Templates, projects and teams the assistant names appear as cards that open them: check the item itself rather than its description.\n- **Read the plan before you run it.** The assistant never changes anything without a plan you see first. Check every step, and click **Cancel** or **Adjust** when one is not what you want. Deleting, inviting and anything that costs credits always wait for your go-ahead.\n- **Ask how it knows.** *Where did you find that?* or *Which page says so?* makes it show its source, or say that it has none.","agent_tools":[]},{"title":"When the Assistant Is Wrong","url":"https://docs.typograph.nl/portal/workspace/assistant-mistakes#what-to-do-when-an-answer-is-wrong","section":"What to Do When an Answer Is Wrong","audience":"portal","headings":[],"text":"1. **Say what is wrong** in the same conversation, such as *That template is in the team Brochures, not Flyers*. The assistant takes it into account in its next answer.\n2. **Ask more precisely.** Name the template, team or page you mean, and the result you expect.\n3. **Start a new conversation** with **+** at the top of the panel when an answer keeps going in the wrong direction: a new conversation starts without what was said before.\n4. **Let us know.** Click the thumbs down under the answer. Under **What went wrong? (optional)**, say what was wrong and click **Send**, or click **Skip** to send the thumbs down alone. We read this feedback to improve the assistant.\n\n[Screenshot: An answer with three documentation cards under it and, after a click on the thumbs down, the box What went wrong? (optional) with the buttons Send and Skip]\n\nDid the assistant change something you did not mean to change? A plan that ran shows **Undo** where the change can be undone. Otherwise, change it back yourself, or ask [support](https://docs.typograph.nl/guides/support/) for help.","agent_tools":[]},{"title":"When the Assistant Is Wrong","url":"https://docs.typograph.nl/portal/workspace/assistant-mistakes#related","section":"Related","audience":"portal","headings":[],"text":"- [Assistant](https://docs.typograph.nl/portal/workspace/assistant) — what the assistant does, and the Act for me switch\n- [Search](https://docs.typograph.nl/portal/workspace/search) — find templates, projects, teams, media and fonts by name","agent_tools":[]},{"title":"Favorites","url":"https://docs.typograph.nl/portal/workspace/favorites","section":null,"audience":"portal","headings":[],"text":"Click the heart on a project or template to pin it under **Favorites** in the sidebar, so it's one click away instead of buried a few levels down the hierarchy.","agent_tools":["favorite_list","favorite_toggle"]},{"title":"Favorites","url":"https://docs.typograph.nl/portal/workspace/favorites#adding-a-favorite","section":"Adding a Favorite","audience":"portal","headings":[],"text":"1. Find the project or template card, or open the item itself.\n\n   [Screenshot: A template card with its empty heart icon in the top right corner]\n\n2. Click its heart icon.\n\n   [Screenshot: An arrow pointing at the heart icon on a template card]\n\nA filled heart means it's in your favorites; click it again to remove it.\n\nFavoriting is per-user — favoriting something doesn't change what your teammates see.\n\n[Screenshot: A template card with its heart icon filled]","agent_tools":["favorite_list","favorite_toggle"]},{"title":"Favorites","url":"https://docs.typograph.nl/portal/workspace/favorites#removing-a-favorite","section":"Removing a Favorite","audience":"portal","headings":[],"text":"Unfavorite a project or template from its own card, wherever you find it:\n\n- **Via the template** — open its project, go to the **Templates** tab, and click the filled heart icon on the template's card.\n\n  [Screenshot: Removing a favorited template from its card in the project's Templates tab]\n\n- **Via the project** — open its team, and click the filled heart icon on the project's card.\n\n  [Screenshot: Removing a favorited project from its card in the team's project list]\n\nEither way, it disappears from **Favorites** in the sidebar right away.","agent_tools":["favorite_list","favorite_toggle"]},{"title":"Favorites","url":"https://docs.typograph.nl/portal/workspace/favorites#finding-your-favorites","section":"Finding Your Favorites","audience":"portal","headings":[],"text":"1. Open the **Favorites** section in the sidebar.\n\nEvery project and template you've favorited shows up here, regardless of which team or project it belongs to — alongside **Recents** and **Shared with me**, the sidebar's other quick-access sections.","agent_tools":["favorite_list","favorite_toggle"]},{"title":"Favorites","url":"https://docs.typograph.nl/portal/workspace/favorites#related","section":"Related","audience":"portal","headings":[],"text":"- [Portal Overview](https://docs.typograph.nl/portal/getting-started/overview) — Favorites, Recents, and Shared with me as sidebar sections","agent_tools":["favorite_list","favorite_toggle"]},{"title":"Notifications","url":"https://docs.typograph.nl/portal/workspace/notifications","section":null,"audience":"portal","headings":[],"text":"Typograph notifies you about activity relevant to you — mentions, invites, requests for access and their answers, and other updates on things you have access to.","agent_tools":["notification_list","notification_mark_read","notification_mark_unread","notification_mark_all_read"]},{"title":"Notifications","url":"https://docs.typograph.nl/portal/workspace/notifications#checking-your-notifications","section":"Checking Your Notifications","audience":"portal","headings":[],"text":"1. Click the notifications icon in the top nav for a quick dropdown, or click **Notifications** for the full list. Unread notifications show a badge on the icon.\n\n   [Screenshot: The notifications bell in the top nav, with an unread badge]\n\n2. Click a notification to jump to what it's about.\n\n   [Screenshot: The notifications dropdown, showing an unread notification]","agent_tools":["notification_list","notification_mark_read","notification_mark_unread","notification_mark_all_read"]},{"title":"Notifications","url":"https://docs.typograph.nl/portal/workspace/notifications#marking-notifications-as-read","section":"Marking Notifications as Read","audience":"portal","headings":[],"text":"1. Open your notifications.\n2. Use the **⋮** menu on a single notification to mark it read (or unread) again, or click **Mark all as read** at the top of the dropdown (in the screenshot above) to clear every unread notification at once.\n\n   [Screenshot: The per-notification menu, with Mark as read highlighted]","agent_tools":["notification_list","notification_mark_read","notification_mark_unread","notification_mark_all_read"]},{"title":"Notifications","url":"https://docs.typograph.nl/portal/workspace/notifications#turning-off-notification-emails","section":"Turning Off Notification Emails","audience":"portal","headings":[],"text":"By default, notifications are also emailed to you. To stop that and only see them in the Portal:\n\n1. Click your avatar in the top-right, then **Profile**.\n\n   [Screenshot: The profile dropdown, with Profile highlighted]\n\n2. Under **Settings**, turn off **Please send me an email for every notification**.\n\n   [Screenshot: The Settings section, with the email notification toggle highlighted]\n\nSee [Account Settings](https://docs.typograph.nl/portal/workspace/account) for the rest of your preferences.","agent_tools":["notification_list","notification_mark_read","notification_mark_unread","notification_mark_all_read"]},{"title":"Notifications","url":"https://docs.typograph.nl/portal/workspace/notifications#related","section":"Related","audience":"portal","headings":[],"text":"- [Account Settings](https://docs.typograph.nl/portal/workspace/account) — turning off notification emails\n- [Invites](https://docs.typograph.nl/portal/organization/invites) — invites to a team, project or template generate a notification; an invitation into an organization comes by email and waits on your home page\n- [Asking for access](https://docs.typograph.nl/portal/users/permissions#asking-for-access) — the admins are notified of a request for a higher role, and whoever asked of the answer, or when the request lapses after 30 days","agent_tools":["notification_list","notification_mark_read","notification_mark_unread","notification_mark_all_read"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search","section":null,"audience":"portal","headings":[],"text":"Search finds the templates, projects, teams, media and fonts you can open, by name, from anywhere in the Portal.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#searching-for-something","section":"Searching for Something","audience":"portal","headings":[],"text":"1. Click **Search** at the top of the Portal, or press <kbd>/</kbd>.\n\n   [Screenshot: The Search button in the middle of the header, with the / shortcut shown on it]\n\n2. The search panel opens. Type at least 2 characters of what you're looking for.\n\n   [Screenshot: Results for spring in the search panel: a template, a project and a media file, with one item in the trash]\n\n3. Click a result to jump straight to it.\n\n   [Screenshot: The Spring poster A3 template highlighted as the first result]\n\nResults are grouped by type:\n\n| Result | Opens |\n|---|---|\n| **Templates** | the template in the Editor |\n| **Projects** | the project's template list |\n| **Media** | the Media tab of the project the file is in |\n| **Fonts** | the Fonts tab of the project the font is in |\n| **Teams** | the team's project list |\n\nTemplates show a small preview of their design, so you can recognise one at a glance. A template that has no preview yet, such as one you have just created and not saved, shows a template icon instead. Media files show as a picture with the name of their project.\n\nEach result shows its place in the hierarchy (which team, which project), so you can tell two similarly-named templates apart. Click the team or project in that path to go there instead.\n\nWhen you search all types, each group shows its first few results. Use **Show all** next to a group's name to see all of that type.\n\nIf nothing matches, the panel says so. Check the spelling, or search for part of a name.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#asking-the-assistant-instead","section":"Asking the Assistant Instead","audience":"portal","headings":[],"text":"When search finds nothing, or what you type reads like a question (it ends with a question mark, or starts with a word such as *how* or *which*), the list ends with **Ask the assistant**. This row only appears when the assistant is available to you.\n\n1. Type your question in search, for example *How do I invite someone to a team?*\n\n   [Screenshot: The search panel with a question typed, no results, and Ask the assistant as the last row, showing the question]\n\n2. Click **Ask the assistant**, or reach it with <kbd>↓</kbd> and press <kbd>Enter</kbd>.\n3. The assistant opens beside the page with your question filled in. Nothing is sent yet: change the text if you like, then send it.\n\n   [Screenshot: The assistant panel open with the question from search in its message field, not yet sent]\n\nThe first time you use the assistant, it first says what it does; your question waits until you click **Start**.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#narrowing-your-search","section":"Narrowing Your Search","audience":"portal","headings":["By Type","By Team or Project","By When It Was Changed"],"text":"Three filters sit under the search field. Each one changes the results as you choose.\n\n### By Type\n\n1. Click **All types**.\n2. Tick the types you want to see. The number next to each type is how many results it holds for what you typed.\n3. Click **Done**.\n\n[Screenshot: The type filter open, listing all types, templates, projects, media, fonts, teams and trash, each with its number of results]\n\nTick **All types** again to see every type.\n\n### By Team or Project\n\n1. Click **Everywhere**.\n2. Type part of a team or project name to shorten the list. You only see the teams and projects you can open.\n3. Tick a team to search all its projects, or tick single projects. You can tick several.\n4. Click **Done**.\n\n[Screenshot: The team and project filter open, with demo typed in its field, showing Demo team and its three projects]\n\nTo search everywhere again, open the filter and click **Clear**.\n\n### By When It Was Changed\n\n1. Click **Modified**.\n2. Choose **Today**, **Last 7 days** or **Last 30 days**.\n\n[Screenshot: The modified filter open with any time, today, last 7 days and last 30 days]\n\nChoose **Any time** to drop this filter.\n\nIf your filters leave nothing to show, the panel says your filters may be hiding what you're looking for, and offers **Clear filters**.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#finding-fonts-and-media","section":"Finding Fonts and Media","audience":"portal","headings":[],"text":"Search finds the media files and fonts in every project you can open. A font opens the Fonts tab of its project, where you can see and manage it.\n\n[Screenshot: Two Inter fonts found for inter, each with the team and project they are in]","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#finding-something-in-the-trash","section":"Finding Something in the Trash","audience":"portal","headings":[],"text":"Search also looks in the trash: teams, projects, and templates that were deleted but are still within their [30-day recovery window](https://docs.typograph.nl/portal/workspace/trash). When you search all types, they are counted in one line at the bottom of the results.\n\n1. Click **Show** on the trash line.\n\n   [Screenshot: A Demo menu template found for menu, with the line 1 in the trash and its Show link at the bottom]\n\n2. The results now show only what is in the trash. Click an item to find it in the trash, where you can restore it.\n\n   [Screenshot: The type filter set to Trash, showing the deleted Brasserie Menu template]\n\nYou can also tick **Trash** in the type filter.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#searching-with-the-keyboard","section":"Searching with the Keyboard","audience":"portal","headings":[],"text":"- Press <kbd>/</kbd> anywhere in the Portal, outside a text field, or <kbd>Ctrl</kbd>+<kbd>K</kbd> (<kbd>⌘</kbd>+<kbd>K</kbd> on a Mac), to open search.\n- Use <kbd>↓</kbd> and <kbd>↑</kbd> to move through the results, and <kbd>Enter</kbd> to open the highlighted one.\n- Press <kbd>Tab</kbd> to reach the filters.\n- Press <kbd>Esc</kbd> to close an open filter, and again to close search.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#reusing-a-recent-search","section":"Reusing a Recent Search","audience":"portal","headings":[],"text":"1. Open search without typing anything. Your last five searches are listed.\n\n   [Screenshot: The search panel without a search, listing three recent searches and, under Recently opened, four templates with their preview and when they were opened]\n\n2. Pick one to search for it again.\n\nTo clear your recent searches, click **Clear All**.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#going-back-to-a-template-you-opened","section":"Going Back to a Template You Opened","audience":"portal","headings":[],"text":"Under **Recently opened**, the search panel shows the last four templates you opened, with their preview and how long ago you opened them. Click one to open it in the Editor, or reach it with <kbd>↓</kbd> and press <kbd>Enter</kbd>.\n\nThe list shows as long as you have not typed anything. Clearing your recent searches leaves it as it is. On a phone it shows the last two.","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Search","url":"https://docs.typograph.nl/portal/workspace/search#related","section":"Related","audience":"portal","headings":[],"text":"- [Organization Structure](https://docs.typograph.nl/portal/organization/structure) — what templates, projects, and teams are\n- [Trash](https://docs.typograph.nl/portal/workspace/trash) — recovering something search found in the trash\n- [Assistant](https://docs.typograph.nl/portal/workspace/assistant) — ask in your own words, and let it do the work after a plan you approve","agent_tools":["search","recent_searches","recent_templates"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash","section":null,"audience":"portal","headings":[],"text":"Deleting a team, project, template, clip, media file, or font doesn't remove it right away — it moves to the trash first, where it stays recoverable for **30 days** before being purged for good.\n\n| Deleted | Recoverable? |\n|---------|:---:|\n| Team | ✓ — 30 days |\n| Project | ✓ — 30 days |\n| Template | ✓ — 30 days |\n| Clip | ✓ — 30 days |\n| Media / Font | ✓ — 30 days, see [Managing Assets](https://docs.typograph.nl/portal/content/assets#managing-uploaded-files) |\n| Form | ✗ — gone at once, and deleted for good after 30 days with the answers entered in it |\n\nThe trash shows what was deleted from the teams you belong to, and the items shared with you directly. Teams, projects and templates can be restored by their admins; clips, media files and fonts by anyone who can edit their project.","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash#restoring-an-item","section":"Restoring an Item","audience":"portal","headings":[],"text":"1. Click **Trash** in the sidebar, under **Shared with me**.\n\n2. Find the item you want back. The trash groups items by kind (teams, projects, templates, clips, media, fonts), with a tab for each kind it holds. A template or image shows its thumbnail beside its name, so you can see which one it is before you restore it.\n\n   [Screenshot: The Templates tab of the trash, listing Landscape Brochure and Brasserie Menu, each with its thumbnail beside its name, its deletion date, in 28 days in the Permanently deleted column, and a Restore button]\n\n   [Screenshot: The Media tab of the trash, listing Spring illustration and Vector_27_77812f, each with its thumbnail beside its name, its deletion date, in 30 days in the Permanently deleted column, and a Restore button]\n\n3. Click **Restore** on its row. On a phone the button shows only its icon.\n\n   An item you may not restore has no **Restore** button. Instead, a line under its name says who can: the team's admins, the project's or template's admins, or the people who can edit its project. Ask one of them to restore it.\n\n   [Screenshot: The trash with a deleted project that reads Only the project's admins can restore it under its name and has no Restore button, and a deleted media file with its Restore button]\n\nAfter restoring, you land where the item lives again: the team, the project, or the project's Templates, Clips, Media, or Fonts tab.\n\nRestoring a team or project brings back everything inside it as it was at the moment of deletion; restoring a single template, clip, media file, or font only brings back that item.\n\nThe templates and files of a deleted project aren't listed in the trash on their own — the project is. Restore the project to get them back. Likewise, if an item's parent team or project was *also* deleted, restore the parent first — you can't restore something into a container that's still in the trash.\nIf you try, the trash says so and names the team or project to restore first, then restore the item.","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash#while-something-is-in-the-trash","section":"While Something Is in the Trash","audience":"portal","headings":[],"text":"Nothing can be done with an item in the trash, or with anything in a team or project in the trash: it can't be opened, edited, shared, commented on, favorited or moved, and nothing can be added to it.\n\nA link to it, from a notification, a bookmark or a colleague, opens a page that names what is in the trash — the item itself, or the team or project it is in — and says when it is deleted for good. For a template, the page shows its thumbnail, so you can see which design the link was for. If you may restore it, the page has a **Restore** button for it: restoring takes you straight back to the page you were opening. If you may not, the page says who can. Either way, **Open the trash** takes you to its row in the trash.\n\n[Screenshot: A page reading This project is in the trash, with the project's name Spring campaign, the lines Nothing in it can be opened, changed or shared until it is restored and It is deleted for good in 28 days, and the buttons Restore project and Open the trash]\n\n[Screenshot: A page reading This template is in the trash, under the template's thumbnail with a trash badge on its corner, with the template's name Brasserie Menu, the lines It cannot be opened, changed or shared until it is restored and It is deleted for good in 28 days, and the buttons Restore template and Open the trash]\n\nA favorite of an item in the trash is hidden until it is restored. A webshop product whose design the shop has already copied stays on sale when the template goes to the trash: the shop sells its own copy, and tells the shopkeeper the design was deleted (see [Design changes](https://docs.typograph.nl/shop/catalogue/design-changes)).","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash#after-30-days","section":"After 30 Days","audience":"portal","headings":[],"text":"Once an item's 30-day window passes, it's purged permanently and can no longer be recovered by anyone, including support — so if you need something back, don't wait.\n\nThe **Permanently deleted** column counts the days down to that moment: an item deleted a moment ago shows *in 30 days*, and one whose 30 days have passed shows *today* until it is purged.","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash#trash-and-storage","section":"Trash and Storage","audience":"portal","headings":[],"text":"Files in the trash don't count toward your organization's storage, up to as much as the plan's storage; only what the trash holds beyond that does. See [Storage and the trash](https://docs.typograph.nl/portal/organization/billing#storage-and-the-trash).","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Trash","url":"https://docs.typograph.nl/portal/workspace/trash#related","section":"Related","audience":"portal","headings":[],"text":"- [Organization Structure](https://docs.typograph.nl/portal/organization/structure) — the **Move to trash** actions that send teams and projects here\n- [Managing Assets](https://docs.typograph.nl/portal/content/assets) — moving media and fonts to the trash\n- [Search](https://docs.typograph.nl/portal/workspace/search) — finding a specific item that's currently in the trash","agent_tools":["trash_list","template_restore","project_restore","team_restore","media_restore","font_restore","clip_restore"]},{"title":"Webshop Back Office","url":"https://docs.typograph.nl/shop/","section":null,"audience":"shop","headings":[],"text":"A Typograph webshop sells personalised print products: a customer picks a card, fills in\ntheir own text and pictures, and orders it printed. The designs come from your Typograph\norganisation; everything else — which designs you sell, at what price, in which categories,\nand the orders that come in — you manage in the shop's **back office**.\n\nThis guide is for the people who run the shop. Building a storefront of your own is covered\nin the [Webshop API](https://docs.typograph.nl/guides/webshop/overview) guide for developers.","agent_tools":[]},{"title":"Webshop Back Office","url":"https://docs.typograph.nl/shop/#where-to-find-it","section":"Where to find it","audience":"shop","headings":[],"text":"The back office is on your shop's own address, followed by `/admin`. For the demo shop\nStamped that is `https://shop-api.typograph.nl/admin`. You sign in with the e-mail address and\npassword of your shop account; see [Your account and roles](https://docs.typograph.nl/shop/getting-started/account).","agent_tools":[]},{"title":"Webshop Back Office","url":"https://docs.typograph.nl/shop/#from-design-to-order","section":"From design to order","audience":"shop","headings":[],"text":"```\nTypograph                     Back office                               Storefront\n┌───────────────┐   copy   ┌──────────────────────────────────────┐   ┌───────────────┐\n│ design + form │ ───────▶ │ product (draft) → price → orderable  │ ─▶│ customer      │\n│ (published)   │          │                                      │   │ fills in and  │\n└───────────────┘          │ orders ◀───────────────────────────────── │ orders        │\n                           └──────────────────────────────────────┘   └───────────────┘\n```\n\n1. A designer makes a design and a form for it in the Typograph portal, and publishes the form.\n2. You add the design to your catalogue. The shop takes **its own copy** of it, and the new\n   product starts as a draft that customers cannot see.\n3. A manager sets the price and makes the product orderable.\n4. Customers fill it in and order it. You see the order in the back office and move it on.\n5. When the design changes in Typograph, the shop keeps selling its copy until somebody\n   approves the new version.","agent_tools":[]},{"title":"Webshop Back Office","url":"https://docs.typograph.nl/shop/#the-sections","section":"The sections","audience":"shop","headings":[],"text":"Down the left side you see the sections your role gives you:\n\n| Section | What it is for |\n|---------|----------------|\n| **Catalogue** | The products you sell, whether customers can order them, and their order in the shop |\n| **Categories and filters** | The departments of your shop, and the filters customers narrow them down with |\n| **Typograph** | **Designs**: everything your Typograph organisation has, to add to the catalogue. **Notifications**: what changed or broke in Typograph |\n| **Orders** | What customers ordered, with their addresses |\n| **Settings** | The owner's screens: **Connection**, **Team** and **Logbook** |\n| **Trash** | Products taken out of the catalogue, which can be put back |\n\nA red number beside **Typograph** means something there is waiting for you.\n\nThe back office speaks English and Dutch; switch with the EN/NL switch. This guide uses the\nEnglish labels, with the Dutch name where people tend to use it, such as *Koppeling* for\nConnection.","agent_tools":[]},{"title":"Webshop Back Office","url":"https://docs.typograph.nl/shop/#guide-sections","section":"Guide sections","audience":"shop","headings":[],"text":"- **[Your account and roles](https://docs.typograph.nl/shop/getting-started/account)** — getting in, and who may do what\n- **[Connection](https://docs.typograph.nl/shop/settings/connection)** — linking the shop to Typograph, and the shop key for your website\n- **[Building the catalogue](https://docs.typograph.nl/shop/catalogue/building)** — from design to orderable product\n- **[Design changes and notifications](https://docs.typograph.nl/shop/catalogue/design-changes)** — approving new versions, and the nightly check\n- **[Handling orders](https://docs.typograph.nl/shop/orders)** — from new to shipped","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building","section":null,"audience":"shop","headings":[],"text":"Everything the shop sells starts as a design in your Typograph organisation. You pick the\ndesigns you want to sell, the shop takes its own copy of each, and a manager decides the price\nand when customers can order it.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#1-find-the-design","section":"1. Find the design","audience":"shop","headings":[],"text":"Go to **Typograph → Designs**. The screen, **Designs in Typograph**, lists every design in the\nprojects the shop uses, in three groups:\n\n| Group | What it means |\n|-------|---------------|\n| **Ready to add** | The design can go into your catalogue |\n| **Already in your catalogue** | You sell it already |\n| **Not for sale yet** | It cannot be added yet. The reason is beside it |\n\nThe reasons a design is not for sale yet, and what to do about them, in the Typograph portal:\n\n- *Never saved. Open the design in Typograph and save it.*\n- *No form. Create one in Typograph.*\n- *The form is a draft. Publish it in Typograph.*\n\nA customer fills in a design through its **form**, so a design without a published form has\nnothing to sell. A design with two published forms appears twice, once per form, with the\nform's name as **Version**. Each becomes a product of its own, and you decide whether you sell\none or both.\n\n**A design is missing?** It is not listed when it sits in a project the shop does not use (the\nowner ticks those under [Connection](https://docs.typograph.nl/shop/settings/connection#which-projects-this-shop-may-see)),\nwhen you switched its project off under **Projects** above the table, or when it belongs to\nanother Typograph organisation.\n\n**Refresh** reads Typograph again. The list is otherwise up to five minutes old, so press it\nwhen a designer has just published something.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#2-add-it-to-the-catalogue","section":"2. Add it to the catalogue","audience":"shop","headings":[],"text":"Press **Add to catalogue**. The design becomes a **product**, as a draft: it is in your\ncatalogue, but customers cannot see it yet.\n\nAt that moment the shop takes **its own copy** of the design: the form, the document, and\nevery font and picture it uses. Customers always personalise and order that copy. So nothing\nyour designers do in Typograph afterwards changes what is on sale, until somebody approves the\nnew version (see [Design changes and notifications](https://docs.typograph.nl/shop/catalogue/design-changes)).\n\nIf the copy could not be taken, for instance because Typograph was briefly unreachable, a\n*Design not copied* notification says why, with a **Try again** button.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#3-fill-in-the-product","section":"3. Fill in the product","audience":"shop","headings":[],"text":"Go to **Catalogue**, which lists the products in your shop, and click a product to open it.\n\n| Field | What it is |\n|-------|------------|\n| **Name** | The product's name in the shop. The address of the product in the shop stays the same when you change it |\n| **Description** | Shown on the product page in the shop |\n| **Categories** | Which departments of the shop this product is in |\n| **Filters** | What customers find this product by when they filter. The price filter is worked out by the shop |\n| **Price** | One price per variant. Only managers and owners see this card |\n\nPress **Save**. If something is marked in red, nothing was saved: fix it and save again.\n\nA new product starts with one variant at a default price, so **set the price before you make\nit orderable**.\n\nThe side of the page shows the design in Typograph, whether the shop has its copy (**Copy\ntaken**), a link to **View in the shop**, and, for managers, the product's **History**.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#4-make-it-orderable","section":"4. Make it orderable","audience":"shop","headings":["The order in the shop"],"text":"In **Catalogue**, the column **Visible to customers** has a switch per product: **Orderable**\nor **Hidden**. Only managers and owners can flip it; others see which it is.\n\nA product whose design broke in Typograph while the shop had no copy of its own shows **No\ncopy** instead of the switch: there is nothing to make it from, so it cannot be put on sale.\nThe notification about it says what happened.\n\n### The order in the shop\n\nDrag the products in **Catalogue** by their handle in the **Sort order** column. The shop\nshows its products in this order under **Recommended**, its default sorting. Turn the filters\nabove the table off to change the order.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#categories-and-filters","section":"Categories and filters","audience":"shop","headings":[],"text":"**Categories and filters** has two tabs.\n\n**Categories** are the departments of your shop; every product is in one or more of them.\nCreate one under **New category** with **Add category**, rename it in its row, and drag the\nrows into the order the shop shows them in. Deleting a category leaves its products alone.\nRenaming does not change the category's address in the shop, so links to it keep working.\n\n**Filters** let a customer narrow down the products in a category, for example by occasion.\nCreate a filter under **New filter** with **Add filter**, then give it options with **Edit\noptions** and **Add option**. You choose a product's options on the product page. Deleting a\nfilter or an option takes it off every product that had it; the products themselves stay. A\nprice filter is always added by the shop itself.","agent_tools":[]},{"title":"Building the Catalogue","url":"https://docs.typograph.nl/shop/catalogue/building#taking-a-product-out-of-the-catalogue","section":"Taking a product out of the catalogue","audience":"shop","headings":[],"text":"In **Catalogue**, open a product's actions and choose **Move to trash**. Only managers and\nowners can. Waiting design updates for it are dropped.\n\n**Trash** lists what was taken out, with **In the trash since** and the number of **Open\norders** still waiting. Products are kept there because orders refer to them. **Restore** puts\na product back in the catalogue as a draft.\n\nTo stop selling a product for a while, set it to **Hidden** instead.\n\n:::info For the operator: copying every design again\nProducts added before the shop kept copies of designs, or whose copy was lost, can be given\none on the server:\n\n```bash\nphp bin/console shop:build-bundles           # products without a copy\nphp bin/console shop:build-bundles --force   # every product, also those with one\n```\n\nIt lists every product it copied or could not copy, and ends with an error when one failed.\n:::","agent_tools":[]},{"title":"Design Changes and Notifications","url":"https://docs.typograph.nl/shop/catalogue/design-changes","section":null,"audience":"shop","headings":[],"text":"The shop sells its own copy of every design (see\n[Building the Catalogue](https://docs.typograph.nl/shop/catalogue/building#2-add-it-to-the-catalogue)). So when a designer changes\na design in Typograph, or deletes it, nothing changes for your customers by itself. The shop\ntells you, under **Typograph → Notifications**, and **a person decides**. Nothing disappears\nfrom your catalogue without somebody choosing so.\n\nEveryone in the back office can read this screen. A red number beside **Typograph** in the\nleft-hand menu says how many items are waiting.","agent_tools":[]},{"title":"Design Changes and Notifications","url":"https://docs.typograph.nl/shop/catalogue/design-changes#a-new-version-of-a-design","section":"A new version of a design","audience":"shop","headings":[],"text":"When a design or its form changes in Typograph, its products get a card on **Notifications**\nsaying what is **New in Typograph**. Only managers and owners see these cards. Several saves\nin one afternoon make one card.\n\nPress **Compare and choose** to see **The design the shop sells now** beside **The design in\nTypograph**, with a list of the **Changes** and the spreads they are on. Then choose:\n\n| Button | What happens |\n|--------|--------------|\n| **Use the new design** (or **Use the new form**) | From now on customers get the new version. Orders already placed keep the version they were ordered with |\n| **Keep the current design** (or **Keep the current form**) | Nothing changes for customers |\n| **Add as a separate product** | The new version becomes a draft next to this product, with the same prices; the current product keeps selling |\n\nIf the new version has a problem that would stop the product from being ordered, the shop\nwarns you before it updates.\n\n**Changed your mind?** An approved update can be taken back from the product's **History**:\nthe product sells the previous version again, and the change is back on **Notifications**.","agent_tools":[]},{"title":"Design Changes and Notifications","url":"https://docs.typograph.nl/shop/catalogue/design-changes#notifications-about-problems","section":"Notifications about problems","audience":"shop","headings":[],"text":"Other cards report something that went wrong in Typograph, or something new there. Each names\nthe products it is about.\n\n| Notification | What happened |\n|--------------|---------------|\n| **Form no longer published** | The form was set back to draft, deleted, or now belongs to another design |\n| **Design deleted** | The design was deleted in Typograph |\n| **Design moved** | The design moved to a project this shop does not use |\n| **Design can no longer be found** | The nightly check could not find the design any more |\n| **Design not copied** | The shop could not take its own copy, with the reason |\n| **New design in Typograph** | A design with a published form appeared in one of the shop's projects |\n| **Typograph unreachable** | The shop could not reach Typograph, so nothing is coming in from it |\n| **Typograph plan below Pro** | The connection is off until the organisation is on Pro again |\n\n**A product with its own copy stays orderable** whatever happened to its design in Typograph:\nthe shop can still make it. A product without a copy cannot be ordered while its design is\nbroken.\n\nWhat you can do on a card:\n\n- **Keep selling** closes the notification and leaves the product as it is.\n- **Take out of the shop** closes it and stops customers from ordering the product. It stays\n  in your catalogue as a draft, ready to put back once the design is fixed. Only managers and\n  owners can.\n- **Try again**, on *Design not copied*, takes the copy again. **Leave it** closes the card.\n- **Add to catalogue**, on *New design in Typograph*, adds the design as a draft. **Do not add**\n  closes the card.\n- **To Connection**, on the two notifications about the connection, takes the owner to\n  [Connection](https://docs.typograph.nl/shop/settings/connection). **Hide** closes the card.\n\nAnswered notifications move to **History**. If a problem is still there, it is marked **still\nhappening**, and **Back on the list** reopens it.","agent_tools":[]},{"title":"Design Changes and Notifications","url":"https://docs.typograph.nl/shop/catalogue/design-changes#the-nightly-check","section":"The nightly check","audience":"shop","headings":[],"text":"Typograph does not announce everything: some changes, such as a form being unlinked, send no\nmessage, and messages can get lost. So every night at 03:00 the shop compares its whole\ncatalogue with Typograph, and puts what it finds on **Notifications**:\n\n- problems with designs the shop sells, including the ones no message announced;\n- new designs in the shop's projects;\n- problems that were solved in the meantime, which it closes by itself.\n\n**The check never changes your catalogue.** It reports; taking a product out of the shop is\nalways somebody's decision. And when Typograph does not answer, or answers with nothing, the\ncheck does nothing at all rather than conclude that every design was deleted.\n\nJust repaired something in Typograph and want to know whether it worked? Press **Check now** on\nthe Notifications screen to run the check straight away. **Refresh** on the Designs and\nCatalogue screens runs it too.\n\n:::info For the operator: the scheduler\nThe 03:00 run needs the `shop-scheduler` worker (`messenger:consume scheduler_shop`) running\non the server under supervisor; when it is down, the check silently does not run. The check\ncan also be run by hand:\n\n```bash\nphp bin/console shop:check-designs\n```\n\nIt ends with an error when it could not read Typograph. The installation and the checks are\nin webshop-service's `deploy/README.md`.\n:::","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account","section":null,"audience":"shop","headings":[],"text":"Everybody who works in the shop has their own account, and a **role** that decides what they\nsee. The shop keeps its own accounts: you do not need a Typograph account to work in the back\noffice.","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account#getting-in","section":"Getting in","audience":"shop","headings":[],"text":"1. **Create an account.** Go to your shop's address followed by `/register`, for the demo shop\n   `https://shop-api.typograph.nl/register`, and fill in your e-mail address, a password of at\n   least 8 characters and, if you like, your name.\n2. **Ask the owner for a role.** A new account can do nothing in the back office until the\n   owner gives it a role under **Settings → Team** (see below).\n3. **Sign in** at `/login`. You land on the screen your role is for: the catalogue, or the\n   orders if that is all you handle.\n\nForgotten your password? The **Forgotten your password?** link on the sign-in page tells you\nwhom to e-mail; the shop does not send recovery e-mails yet.\n\n:::info For the operator: the first owner\nRoles are handed out on the Team screen, and only an owner can open it. So the very first\nowner of a new shop is made on the server, once that person has created their account:\n\n```bash\nphp bin/console shop:grant-role owner@example.com owner\n```\n\nThe role is `owner`, `manager`, `catalogue` or `orders`, and `owner` when left out. After the\nfirst owner, use the Team screen.\n:::","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account#the-roles","section":"The roles","audience":"shop","headings":[],"text":"Four roles, drawn along the two lines that matter in a shop: who touches the prices, and who\nsees a customer's address.\n\n| Role | In short | Can | Cannot |\n|------|----------|-----|--------|\n| **Owner** | Everything, plus the connection with Typograph and who is in the team | Put the catalogue together and set prices · manage the connection with Typograph · grant and revoke roles · view orders and customer data | — |\n| **Manager** | The catalogue and the prices. Sees orders and customer data | Put the catalogue together · make products visible and set prices · view orders and customer data | Manage the connection with Typograph · grant roles |\n| **Catalogue management** | Fills the catalogue with designs | Prepare designs from Typograph as drafts · update names, categories and images | Set prices or make anything visible · view orders or customer data |\n| **Customer support** | Handles orders and sees customer data | Handle orders and update their status · view customer data and addresses | Touch the catalogue or the prices |\n\nSome things that follow from the table:\n\n- An **owner** holds every other role as well.\n- A **manager** also does everything *Catalogue management* does, and reads orders, but only\n  *Customer support* (or the owner) changes an order's status.\n- Only a manager or the owner takes a product out of the catalogue, approves a new version of\n  a design, or reads the **Logbook**.\n- **Notifications** are readable by everyone in the back office: a design that broke is a\n  product that cannot be made, which is news for whoever promised it to a customer.\n\nThe same list is on the Team screen, under **What the roles may do**.","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account#the-team-owner-only","section":"The team (owner only)","audience":"shop","headings":["Projects per colleague"],"text":"**Settings → Team** lists everyone with a role, with their roles beside them.\n\n**Give someone a role.** In the **Give someone a role** card, enter the e-mail address of\ntheir account, choose a **Role** and press **Add**. They have to have created their account\nfirst; otherwise the screen says there is no account with that address.\n\n**Change a role** with the role list in the person's row; the change is saved straight away.\nChoose **No access** to take them out of the back office. Their account stays, and they keep\nit as a customer of the shop.\n\n**You cannot remove the last owner.** If you are the only owner, make someone else an owner\nfirst: a shop without an owner has nobody left who can hand out roles.\n\n### Projects per colleague\n\nBy default everyone works with every Typograph project the shop uses: their row says\n**All projects**. Press that button to choose the projects they work with instead. They then see, and can add, only the designs\nin those projects. Ticking every project gives them all projects again, including ones the\nshop starts using later; ticking none is not possible.","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account#the-logbook","section":"The logbook","audience":"shop","headings":[],"text":"Every decision about what the shop sells and who works in it is written down: products added,\nedited, published, hidden, moved to the trash and restored, price changes, design updates\napproved, kept or undone, notifications handled, categories and filters, order status\nchanges, roles and projects, and changes to the connection. **Settings → Logbook** shows who\ndid what and when, newest first. Managers find it in the left-hand menu as **Logbook**, and a\nproduct's own history is on its page in the catalogue.","agent_tools":[]},{"title":"Your Account and Roles","url":"https://docs.typograph.nl/shop/getting-started/account#usage-analytics","section":"Usage analytics","audience":"shop","headings":["In the back office","On the storefront"],"text":"Typograph measures how the back office and the storefront are used, to see where they can be\nclearer. Nothing is recorded until you, or a visitor of the shop, say yes. The details are in the\n[privacy statement](https://typograph.nl/privacy).\n\n### In the back office\n\nThe first time you open a back-office page after signing in, a dialog asks whether we may see\nhow you use the back office.\n\n![The dialog \"Help us improve the back office\" over the catalogue, with an \"Allow usage analytics\" button and a \"No, thanks\" link](https://docs.typograph.nl/img/shop/analytics-consent-dialog.png)\n\n- **Allow usage analytics** records your screen while you work in the back office, linked to your\n  user id and the shop, never to your name or email address, and keeps an identifier in your\n  browser. What you type is never recorded, and the text of pages about people (orders, the team,\n  the logbook, feedback and the connection) is hidden in a recording.\n- **No, thanks** keeps it anonymous: page visits and clicks are counted without anything being\n  stored on your device, apart from your answer.\n\nThe dialog asks once: your answer is kept with your account, so it follows you to every browser.\nTo change it, open the menu under your name in the top right and turn **Usage analytics** on or\noff. The change applies straight away: you stay on the same page, and a message confirms it.\n\n![The account menu with the \"Usage analytics\" switch above Sign out](https://docs.typograph.nl/img/shop/analytics-consent-switch.png)\n\n### On the storefront\n\nVisitors of the storefront see a banner at the bottom of the page the first time they visit.\n**Accept** and **Decline** are the same size and weight. Until a visitor accepts, visits are\nonly counted anonymously. After accepting, their visit is recorded, without what they type,\ntheir address, their payment details or the text they put on a card. The banner remembers the\nanswer in the visitor's browser; **Cookie settings** in the footer opens it again to change it.\n\n![The storefront's banner \"May we see how you use the shop?\" with Accept and Decline buttons](https://docs.typograph.nl/img/shop/analytics-consent-banner.png)","agent_tools":[]},{"title":"Handling Orders","url":"https://docs.typograph.nl/shop/orders","section":null,"audience":"shop","headings":[],"text":"**Orders** shows what customers have ordered. It is open to *Customer support*, managers and\nowners; *Catalogue management* does not see orders, because they hold customers' addresses.","agent_tools":[]},{"title":"Handling Orders","url":"https://docs.typograph.nl/shop/orders#the-list","section":"The list","audience":"shop","headings":[],"text":"The 100 most recent orders, newest first, with their number, the **Customer**, the\n**Lines**, the **Payment method**, the **Total** and the status. The buttons above the list\nshow **All** orders, or only the orders with one status.","agent_tools":[]},{"title":"Handling Orders","url":"https://docs.typograph.nl/shop/orders#one-order","section":"One order","audience":"shop","headings":[],"text":"Open an order to see:\n\n- **Lines**: each product with its variant, the **Quantity**, the price **Each** and the line\n  total, with the **Subtotal**, **Shipping**, **VAT** and **Total** below;\n- the **Customer**'s e-mail address, and their note if they left one;\n- the **Delivery address** and the **Billing address**;\n- **Payment**: how the customer chose to pay (**Via**), and the **Reference** they gave — the\n  bank for iDEAL, the purchase order number for an invoice.\n\nEach line is the customer's own personalised design, kept exactly as it was ordered. A later\nchange to the design in Typograph, approved or not, never changes an order already placed.","agent_tools":[]},{"title":"Handling Orders","url":"https://docs.typograph.nl/shop/orders#the-print-file-of-a-line","section":"The print file of a line","audience":"shop","headings":[],"text":"Every personalised line gets a print-ready PDF, made by Typograph a few minutes after the\norder is placed. Under each line you see how it is doing: **Being made** (with a spinner, and\nupdated without a reload), **Ready** with **Download PDF**, or **Failed** with the reason.\n**Regenerate PDF** makes it again; it is there once a file is ready or failed, for *Customer\nsupport* and owners. A print file that fails for good also appears under **Notifications**,\nwith **Go to order**.\n\nThe customer can download the same PDF from their own order page in the storefront: right\naway, or once the order is paid, as you choose under\n[Settings → Print files](https://docs.typograph.nl/shop/settings/print-files).","agent_tools":[]},{"title":"Handling Orders","url":"https://docs.typograph.nl/shop/orders#moving-an-order-on","section":"Moving an order on","audience":"shop","headings":[],"text":"Choose the new status under **Set to** and press **Save status**.\n\n| Status | Meaning |\n|--------|---------|\n| **New** | Just placed |\n| **Paid** | The payment is in |\n| **In production** | Being printed |\n| **Shipped** | On its way to the customer |\n| **Cancelled** | Will not be made |\n\nOnly *Customer support* and owners change a status; a manager reads orders but cannot move\nthem. Every change is written in the **Logbook**, with who made it.\n\n**The shop does not take payments itself.** It records which way the customer chose to pay;\nmoving an order to **Paid** is done here, by hand, once the money is in.","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection","section":null,"audience":"shop","headings":[],"text":"**Settings → Connection** (*Koppeling* in Dutch) is where the shop is linked to your Typograph\norganisation, and where your website gets the key it uses to show the catalogue and take\norders. Only an owner can open it: the key on this screen can read every design in the\norganisation.\n\nThe screen has four cards.","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection#the-app-key","section":"The app key","audience":"shop","headings":[],"text":"The shop reads your designs with an **app key**, made in the Typograph portal.\n\n1. In the portal, go to **Organisation → Apps** and create an app for this shop. Give it three\n   permissions: `file:read`, to read the designs, `subscription:read`, to check your plan,\n   and `publisher`, to make the print files of orders.\n   Use one app per shop: then you can revoke one without upsetting the others, and the\n   portal's logs say which shop did what.\n2. Copy the app's **Client ID** and **Client secret** into the card on this screen.\n3. Press **Connect and check**. The shop tries the key straight away.\n\nThe badge at the top of the card says **Connected since …** when it worked, or **Not\nconnected yet** with the reason beside it, such as a key that may not read designs.\n\n**The shop needs Typograph Pro or higher.** A key from an organisation on a lower plan is\nrefused. If the organisation later drops below Pro, the shop hears it from Typograph, switches\nthe connection off and puts a *Typograph plan below Pro* notification at the top of\n**Notifications**; back on Pro, it switches on again by itself.\n\n**The secret is shown once.** After saving, the card only says **Set on …**. To use a new\nsecret, rotate it in the portal, press **Replace**, paste it and connect again. Closing the box\nwithout pasting anything keeps the current one.","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection#which-projects-this-shop-may-see","section":"Which projects this shop may see","audience":"shop","headings":[],"text":"The key belongs to the organisation, so it never sees beyond it. These ticks make its reach\nsmaller: a shop that only sells Christmas cards does not need to see the rest. Only designs in\nticked projects can be added to the catalogue. Leaving every box empty is the same as ticking\nthem all.\n\nTo narrow it further per colleague, see\n[Projects per colleague](https://docs.typograph.nl/shop/getting-started/account#projects-per-colleague).","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection#notifications","section":"Notifications","audience":"shop","headings":[],"text":"When a design changes in Typograph, Typograph tells the shop, so it never sells an old\nversion without you knowing. For that, Typograph needs to know where to send its messages.\n\n1. In the portal, create a **webhook** for the organisation, with the address shown under\n   **Receiving address**: your shop's address followed by `/webhooks/typograph`, for the demo\n   shop `https://shop-api.typograph.nl/webhooks/typograph`. Subscribe it to the design, form,\n   project and plan events (`template.*`, `form.*`, `project.deleted`, `subscription.*`).\n2. Copy the webhook's signing secret into **Signing secret**, and press **Save**.\n\nWithout the signing secret the shop accepts no message at all: it would have no way to tell\nTypograph's messages from anyone else's. Developers find the details of the messages in\n[Webhooks](https://docs.typograph.nl/guides/api/webhooks).\n\nThe tick boxes say which messages the shop acts on:\n\n| Message | What the shop does |\n|---------|--------------------|\n| A design has changed | For a product with its own copy, the new version waits under **Notifications** for someone to approve it |\n| A form has changed | The same. A form that is no longer published, or now belongs to another design, becomes a notification |\n| A design has been deleted | A notification. A product with its own copy stays orderable until you decide |\n| A form has been deleted | A notification, the same way |\n| A project has been deleted | The shop checks the whole catalogue against Typograph straight away |\n| The Typograph plan has changed | Below Pro the connection goes off; back on Pro it comes on again |\n\nThe first two can be switched off. The others are marked **always on**: they are how the shop\nlearns that it can no longer make something it sells.\n\nMessages can get lost, so the shop does not depend on them alone: every night it compares the\nwhole catalogue with Typograph (see [The nightly check](https://docs.typograph.nl/shop/catalogue/design-changes#the-nightly-check)).","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection#shop-key","section":"Shop key","audience":"shop","headings":[],"text":"Your website uses the **shop key** to fetch your catalogue and place orders. It belongs in the\npages of your website and may be read by anyone: the list under it decides who may use it.\n\n**Allowed websites.** Enter every address your storefront runs on, one per line, such as\n`https://kaarten.nl`. Only these websites can use the key. The list starts empty, which means\nno website can, so a new shop is not open to anybody until you fill it in. Give the addresses\nas your web developer gives them to you: the start of the address, without a path.\n\n**Create a new key** if the key should stop working, for instance when you move to a new\nwebsite. Every website that uses the current key stops working at once, until it has the new\none, so agree on the moment with whoever builds your website.\n\nFor developers: the key and the list are explained in\n[Webshop API → Authentication](https://docs.typograph.nl/guides/webshop/overview#authentication).","agent_tools":[]},{"title":"Connection","url":"https://docs.typograph.nl/shop/settings/connection#what-is-logged","section":"What is logged","audience":"shop","headings":[],"text":"Every change on this screen is written in the **Logbook**: which part changed, by whom and\nwhen. The keys and secrets themselves are never written down.","agent_tools":[]},{"title":"Print files","url":"https://docs.typograph.nl/shop/settings/print-files","section":null,"audience":"shop","headings":[],"text":"**Settings → Print files** (*Drukbestanden* in Dutch) decides when a customer can download the\nprint-ready PDF of their order from their order page in the storefront. Only an owner can open\nit.\n\nThe screen has one switch: **Customers may download their print files before the order is\npaid** (*Klanten mogen hun drukbestanden downloaden voordat de bestelling betaald is*).\n\n- **On** — the default, for a demonstration: a customer downloads each PDF as soon as it is\n  ready, whatever the order's status.\n- **Off** — the PDF waits for the payment. Until you move the order to **Paid** (or further),\n  the customer's order page says the file is ready and can be downloaded once the order is\n  paid. **Paid**, **In production** and **Shipped** orders hand it out.\n\nA **Cancelled** order never hands out its PDF, whichever way the switch stands.\n\nThe shop does not take payments itself, so with the switch off a customer gets the PDF once\nyou have moved the order to **Paid** on the [order screen](https://docs.typograph.nl/shop/orders#moving-an-order-on).","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/","section":null,"audience":"api","headings":[],"text":"Welcome to the Typograph Developer Documentation.\n\nTypograph turns templates into press-ready PDFs and images: business cards, name badges, posters, invitations, direct mail. A designer builds the template and decides what may change in it. The people who fill it in personalise within those limits, whether they use a form on your site, your own system through the API, or an AI agent through the MCP server.","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/#who-decides-what-can-change","section":"Who decides what can change","audience":"api","headings":[],"text":"The template does. In the [Editor](https://docs.typograph.nl/guides/integration/editor), the designer gives each element a role: *Fixed*, *Fill in the text*, *Place a picture*, *Fill in and place*, *Full edit*, or a role of their own, with limits on how far an element may be moved or resized. The designer can also lock elements and choose which fields a [form](https://docs.typograph.nl/guides/integration/form) exposes.\n\nWhichever route fills a template in, the print file comes from the same template and the same [publisher](https://docs.typograph.nl/guides/api/publisher).\n\n:::note What enforces the rules today\nThe [Viewer](https://docs.typograph.nl/guides/integration/viewer) applies the template's roles and limits in the browser, and the [Form Widget](https://docs.typograph.nl/guides/integration/form) offers only the fields its form defines. Enforcing the same rules on the server for every API and MCP call is planned. Until then, a client with write access can change elements that a role protects, so give agents and integrations only the scopes they need.\n:::","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/#what-youll-find-here","section":"What You'll Find Here","audience":"api","headings":[],"text":"- **[Guides](https://docs.typograph.nl/guides/getting-started/installation)** — step-by-step tutorials for integrating the browser scripts and the REST API\n- **[Portal Documentation](https://docs.typograph.nl/portal/)** — end-user guides for the Typograph Portal\n- **[OpenAPI Reference](https://docs.typograph.nl/api-reference)** — interactive Swagger UI for the REST API","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/#platform-surface","section":"Platform Surface","audience":"api","headings":[],"text":"Typograph exposes its capabilities through three complementary surfaces:\n\n| Surface | Audience | Documented In |\n|---------|----------|---------------|\n| **REST API** | Any backend or client that can speak HTTP | [Guides → API](https://docs.typograph.nl/guides/api/overview) |\n| **Browser scripts** | Web apps embedding the Editor, Viewer or Form Widget | [Guides → Integration](https://docs.typograph.nl/guides/integration/editor) |\n| **MCP Server** | AI agents (Claude, ChatGPT, Copilot, Cursor and others), acting for a signed-in user | [AI Agents (MCP)](https://docs.typograph.nl/guides/api/mcp-server) |\n\nThe REST API and the MCP server both go through the same **API Gateway**, which handles OAuth 2.0 authentication, scope-based authorization, tiered rate limiting, and routing to the underlying services. The browser scripts run in your page and reach Typograph through the host providers you pass them. An agent connected through MCP works with the rights of the user who connected it.","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/#browser-scripts","section":"Browser Scripts","audience":"api","headings":[],"text":"The Editor and Viewer are delivered as CDN-hosted scripts — no npm package install is required.\n\n| Script | Purpose | CDN URL |\n|--------|---------|---------|\n| Editor | Full document editing UI | `https://cdn.typograph.nl/editor/latest/editor.js` |\n| Viewer | Read-only template filling UI | `https://cdn.typograph.nl/editor/latest/viewer.js` |\n| Form Widget | A form with a live preview, for end users | `https://cdn.typograph.nl/form/latest/form.js` |\n\nThe Editor and Viewer are constructed from the same bundle family and expose a `window.typograph` global, communicating with your page through DOM events and the host providers you pass at construction. See [Editor Integration](https://docs.typograph.nl/guides/integration/editor) and [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer).","agent_tools":[]},{"title":"Introduction","url":"https://docs.typograph.nl/guides/#quick-links","section":"Quick Links","audience":"api","headings":[],"text":"- [Installation](https://docs.typograph.nl/guides/getting-started/installation) — create your first OAuth client and obtain credentials\n- [Quick Start](https://docs.typograph.nl/guides/getting-started/quick-start) — obtain a token and call the API\n- [Authentication](https://docs.typograph.nl/guides/api/authentication) — OAuth 2.0 flows (Client Credentials, Authorization Code + PKCE, Refresh)\n- [Core Concepts](https://docs.typograph.nl/guides/concepts/documents) — the document model (templates, pages, elements, swatches)\n- [Support](https://docs.typograph.nl/guides/support) — contact, FAQ, status","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication","section":null,"audience":"api","headings":[],"text":"The Typograph API uses OAuth 2.0 for authentication. Choose the appropriate flow based on your use case.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#quick-start","section":"Quick Start","audience":"api","headings":[],"text":"For server-to-server integrations, use **Client Credentials**:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=file document publisher converter\"\n```\n\n**Response:**\n\n```json\n{\n  \"access_token\": \"typ_abc123...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 172800,\n  \"scope\": \"file document publisher converter\"\n}\n```","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#getting-credentials","section":"Getting Credentials","audience":"api","headings":["OAuth Client Registration","App logo","When signing in to your app fails","IP allowlist"],"text":"### OAuth Client Registration\n\n1. Log in to the [Typograph Portal](https://portal.typograph.nl).\n2. Open your organization — **Settings → Organizations → _your organization_**.\n3. Go to the **Apps** tab and click **+ Create App**.\n4. Fill in the form:\n   - **Name** — a human-readable name for the app.\n   - **Redirect URIs** — add one or more callback URLs if you're building a user-facing app that will use the Authorization Code flow. Leave empty for a backend-only app using Client Credentials.\n   - **Scopes** — tick the scopes your app needs (see the [Scopes reference](https://docs.typograph.nl/guides/api/scopes)). The available scopes depend on your organization's subscription.\n5. Save. The portal displays your **Client ID** and **Client Secret** once — copy both immediately.\n\n:::caution\nThe **Client Secret** is shown only once at creation. Store it in a secret manager or environment variable; the portal can't show it again.\n:::\n\n:::note Grant type is inferred\n- App with **no redirect URIs** → `client_credentials` grant (server-to-server).\n- App with **one or more redirect URIs** → `authorization_code` + `refresh_token` grants (user-facing).\n:::\n\n### App logo\n\nA user-facing app (one with a redirect URI) can show its logo on the consent screen, where people decide whether to give it access to their Typograph account. Without one, they see a grey placeholder.\n\n1. In the **Apps** tab, open the **⋯** menu of the app and choose **Logo**.\n2. Choose an image: a **square** PNG, JPG or WebP of **128 to 1024 pixels per side**, at most **512 KB**. 256 × 256 pixels looks sharp everywhere. The portal shows it before you save and says which requirement an image misses.\n3. Click **Save logo**. **Remove logo** brings back the placeholder.\n\nOnly an owner or admin of the organization can change an app's logo. Typograph does not crop or resize the image, so make it square yourself, with some space around the mark.\n\n### When signing in to your app fails\n\nPeople who sign in to your app may never have heard of Typograph. When their sign-in fails on Typograph's side, after they have seen the consent screen (granting fails, they decline and your app registered no address to send them back to, or the request expired), the error page sends them to you first:\n\n- **Its button** takes them back to your app's website, with the host shown under it.\n- **Its help section** says *Contact &lt;your app&gt;*, with a mail to your organization's **Contact email** that has the error reference filled in. Without one, it points to your website.\n- **Typograph's support** comes after that, smaller, because Typograph runs the sign-in.\n\nThe address and website come from your organization's **Settings** in the portal (**Contact email**, **Website**), so anyone who signs in to one of your apps can see them there. An app that registers itself with a client ID metadata document (its `client_id` is the document's URL, as MCP clients do) uses its `client_uri` as the website, but only when it is on the same host as the document. Without a contact email or website, the page offers Typograph's support only, and no button. A broken authorization link (a missing `redirect_uri`, say) names no app: anyone can make such a link.\n\n### IP allowlist\n\nAn app that runs on your own servers (a confidential client) can be limited to the addresses those servers use. Its client credentials and its tokens then work from those addresses only.\n\n1. In the **Apps** tab, open the **⋯** menu of the app and choose **IP addresses**.\n2. Write one address or CIDR range per line, IPv4 or IPv6 (`203.0.113.7`, `203.0.113.0/24`, `2001:db8::/32`), at most 50.\n3. Click **Save addresses**. **Allow every address** lifts the limit.\n\nTypograph checks the address twice:\n\n- **At the token endpoint.** From another address, `POST /oauth/token` answers `401` with `invalid_client`, for every grant: client credentials, an authorization code and a refresh token.\n- **On every API request.** A token used from another address gets `403` with `ip_not_allowed`. That holds for tokens issued before the list was set too, within a minute of saving.\n\nThe address is the one your request reaches Typograph from: behind a NAT gateway or a proxy, that is the gateway's public address. A public client (a browser or mobile app) cannot have a list. Only an owner or admin of the organization can change it, in the portal; the app object shows it as `allowed_ips`, `null` without one.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#oauth-20-flows","section":"OAuth 2.0 Flows","audience":"api","headings":["Client Credentials Flow","Authorization Code Flow with PKCE","Refresh Token Flow"],"text":"### Client Credentials Flow\n\nBest for server-to-server integrations where no user interaction is needed.\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=publisher converter document\"\n```\n\n### Authorization Code Flow with PKCE\n\nBest for web applications and mobile apps where users authenticate.\n\n#### Step 1: Generate PKCE Parameters\n\n```javascript\n// Generate code_verifier (43-128 characters)\nconst codeVerifier = generateRandomString(64);\n\n// Generate code_challenge\nconst encoder = new TextEncoder();\nconst data = encoder.encode(codeVerifier);\nconst digest = await crypto.subtle.digest('SHA-256', data);\nconst codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))\n  .replace(/\\+/g, '-')\n  .replace(/\\//g, '_')\n  .replace(/=/g, '');\n```\n\n#### Step 2: Redirect to Authorization\n\n```\nhttps://api.typograph.nl/oauth/authorize\n  ?response_type=code\n  &client_id=YOUR_CLIENT_ID\n  &redirect_uri=https://your-app.com/callback\n  &scope=openid profile email file\n  &state=random_state_string\n  &code_challenge=CODE_CHALLENGE\n  &code_challenge_method=S256\n```\n\nThe consent screen shows the scopes your app asks for and, for a user in more than one organization, a choice under **Authorize on behalf of**. The token reaches only the organization picked there ([which one a token reaches](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)); a user in one organization authorizes for that one.\n\nThe user has 30 minutes from this redirect to sign in and answer the consent screen. After that the request has expired: no code is issued, Typograph tells the user so, and its button takes them back to your app at your registered `redirect_uri` with `error=access_denied`, your `state`, and `error_reason=authorization_request_expired`. The error is the standard one a denied consent gets, so an app that knows only the OAuth error codes keeps working; `error_reason` tells the two apart, so your app can tell the user that signing in took too long rather than that they cancelled. A denied consent has no `error_reason`. Read only the values you know, ignore any other, and never show `error_description` or `error_reason` to the user as they come: they are in the URL, which anyone can change. Start a new authorization request then. A `redirect_uri` your app did not register gets no way back; the user is offered the Typograph portal instead.\n\n#### Step 3: Exchange Code for Tokens\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=authorization_code\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"code=AUTHORIZATION_CODE\" \\\n  -d \"redirect_uri=https://your-app.com/callback\" \\\n  -d \"code_verifier=CODE_VERIFIER\"\n```\n\nSend the same `redirect_uri` as in step 2: without it, or with another one, the answer is `400 invalid_grant`. A code works once.\n\n**Response:**\n\n```json\n{\n  \"access_token\": \"typ_abc123...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 172800,\n  \"refresh_token\": \"typ_refresh_xyz789...\",\n  \"scope\": \"openid profile email file\"\n}\n```\n\n### Refresh Token Flow\n\nExchange a refresh token for a new access token:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=refresh_token\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"refresh_token=YOUR_REFRESH_TOKEN\"\n```\n\n:::info\nRequest the `offline_access` scope to receive refresh tokens.\n:::\n\nA refresh token works **once**. The response carries a new refresh token: store it in place of the old one, and use it for the next refresh. Refreshing also ends the access token the old refresh token came with.\n\n- Only the client the refresh token was issued to can use it; any other client gets `400 invalid_grant`.\n- Sending the same refresh token again within a minute (a retry after a lost response) gets `400 token_reuse_grace_period`; use the tokens from the first response.\n- Sending it again later is treated as theft: every token of that sign-in is revoked, the answer is `400 token_reuse_security_breach`, and the user signs in again.\n- A sign-in lasts at most 30 days, however often it is refreshed. A refresh 30 days or more after the user signed in gets `400 invalid_grant` with `\"reason\": \"sign_in_expired\"`: send the user through the [authorization flow](https://docs.typograph.nl/guides/api/authentication#authorization-code-flow-with-pkce) again. The access token from the last refresh before then works until it expires.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#using-access-tokens","section":"Using Access Tokens","audience":"api","headings":[],"text":"Include the access token in the `Authorization` header:\n\n```bash\ncurl https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#token-types","section":"Token Types","audience":"api","headings":[],"text":"The Typograph API distinguishes between **user tokens** (issued via `authorization_code` / `refresh_token`) and **client tokens** (issued via `client_credentials`). Each gateway route requires one or the other, or accepts either.\n\nSee [Token Types](https://docs.typograph.nl/guides/api/token-types) for the full breakdown of which endpoints require which token type and how to verify a token's type via introspection.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches","section":"Which organization a token reaches","audience":"api","headings":[],"text":"A token reaches **one organization**: the organization the user picked when they authorized your app, or, when they picked none and for a client token, the organization that owns your app. Introspection names it in `client_organization_id`.\n\nEvery call acts on that organization. A request that names another one is refused with `403` and `organization_not_bound`, also when the user is a member or an owner of that other organization:\n\n- an organization in the path, such as `/v1/identity/organizations/{id}/members` or `/v1/file/organizations/{organizationId}/teams`;\n- the `organization_id` query parameter, such as `GET /v1/identity/oauth/clients?organization_id=…`;\n- the `X-Typograph-On-Behalf-Of-Organization-Id` header;\n- a team, project, template, comment, media item, font, clip or upload of another organization, named by its own ID, and a move or copy into another organization (`team_id` or `project_id` in the body). A form of another organization, and the item a move starts from, answer `404`, as an ID that does not exist.\n\nThe file service's lists hold only that organization: teams, what is shared with the user, search, trash, favorites, recently opened templates, notifications and the invitations to teams, projects and templates (`GET /v1/file/users/me/invitations`). The user's invitations into organizations (`GET /v1/identity/users/me/invitations`) are theirs, not an organization's, and list every one. A person who is only in another organization is not found (`404`).\n\nAn app of another organization, read by its own ID, shows only its public details, and its logo cannot be changed (`404`).\n\n**One way out, for reading only.** An organization can let the apps of its guests read its teams ([Apps of guests](https://docs.typograph.nl/guides/api/identity-service#apps-of-guests)): a user with a role in one of that organization's teams then reads, with your app's token, the teams, projects and templates they have a role on there, and the media, fonts and clips those designs use. Reading means a `GET`, or asking for a presigned URL to download (`action` other than `PutObject`). Everything else in that organization stays refused with `organization_not_bound`, whatever the user's role there, and the lists then hold those teams too, with `organization_name` naming their organization. Jobs your app starts are billed to the token's own organization.\n\n```json\n{\n  \"error\": \"organization_not_bound\",\n  \"error_description\": \"This token reaches only the organization it was granted for\"\n}\n```\n\nTo work in another organization, send the user through the [authorization code flow](https://docs.typograph.nl/guides/api/authentication#authorization-code-flow-with-pkce) again: the consent screen asks every time, and a user who belongs to more than one organization picks under **Authorize on behalf of** which one the token is for. You then hold a token per organization. Listing the user's organizations (`GET /v1/identity/organizations`) still shows all of them, with the user's role in each.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#tokens-for-a-resource","section":"Tokens for a resource","audience":"api","headings":[],"text":"A client may name the resource it wants a token for with the `resource` parameter (RFC 8707) at `/oauth/authorize` and `/oauth/token`; the token then carries it as its `audience`. The API accepts a token without an audience, and one whose audience is the API (`https://api.typograph.nl`) or the [MCP server](https://docs.typograph.nl/guides/api/mcp-server) (`https://mcp.typograph.nl`), with or without a path. A token for any other resource is not meant for this API and is refused:\n\n```json\n{\n  \"error\": \"invalid_audience\",\n  \"error_description\": \"Token audience does not match this API\"\n}\n```\n\nwith `401`. Leave `resource` out, or name one of these two, when the token is for the Typograph API.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#token-management","section":"Token Management","audience":"api","headings":["Token Lifetimes","Token Introspection","Token Revocation"],"text":"### Token Lifetimes\n\n| Token Type | Lifetime |\n|------------|----------|\n| Access Token | 2 days (172,800 seconds) |\n| Refresh Token | 3 days (259,200 seconds), renewed by every refresh |\n| Sign-in | 30 days from when the user signed in: refreshing no longer extends it after that |\n| Authorization request (from the redirect to the authorization endpoint until the consent) | 30 minutes |\n| Authorization Code | 10 minutes |\n\n### Token Introspection\n\nVerify token validity and get metadata. Authenticate as your client, with `client_id` and `client_secret` in the body or with HTTP Basic; without them the answer is `401 invalid_client`. Only a confidential client (one with a secret) can introspect: a public client gets `401 invalid_client`. You learn about your own client's tokens only: another client's token answers `{\"active\": false}`, as an unknown one does.\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/introspect \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=YOUR_ACCESS_TOKEN\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\"\n```\n\n**Response:**\n\n```json\n{\n  \"active\": true,\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"token_type\": \"Bearer\",\n  \"grant_type\": \"authorization_code\",\n  \"auth_type\": \"user\",\n  \"scope\": \"file document publisher\",\n  \"exp\": 1734567890,\n  \"iat\": 1734395090,\n  \"sub\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n  \"client_organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"organization_binding\": \"token\"\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `grant_type` | How the token was obtained: `authorization_code`, `client_credentials`, or `refresh_token` |\n| `auth_type` | Token type: `user` (has user context) or `client` (no user context) |\n| `sub` | User ID (only present for user tokens) |\n| `client_organization_id` | The organization the token reaches ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)) |\n| `organization_binding` | `token` when the user picked that organization at consent, `client` when it is the organization that owns your app |\n\n### Token Revocation\n\nInvalidate a token when no longer needed:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/revoke \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=YOUR_ACCESS_TOKEN\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\"\n```\n\nA public client (an app without a client secret, such as a desktop app or an MCP client using PKCE) revokes its own tokens with its `client_id` alone:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/revoke \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=YOUR_ACCESS_TOKEN\" \\\n  -d \"client_id=YOUR_CLIENT_ID\"\n```\n\nA client can only revoke tokens issued to it. The answer is `200` either way, also for a token that is unknown or belongs to another client.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#single-sign-on-sso","section":"Single Sign-On (SSO)","audience":"api","headings":[],"text":"Typograph's own login screen supports SSO via Google and Microsoft Azure AD. From your application's perspective, SSO is **invisible** — you use the standard [Authorization Code + PKCE](https://docs.typograph.nl/guides/api/authentication#authorization-code-flow-with-pkce) flow exactly as described above. When the user hits the Typograph login screen they see the \"Continue with Google\" / \"Continue with Microsoft\" buttons next to signing in with a code sent by email, pick whichever they prefer, and are redirected back to your `redirect_uri` with an authorization code as normal.\n\nYou don't need to build anything SSO-specific into your app.","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#user-information","section":"User Information","audience":"api","headings":[],"text":"Get the authenticated user's profile (requires `openid` scope):\n\n```bash\ncurl https://api.typograph.nl/oauth/userinfo \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\n**Response:**\n\n```json\n{\n  \"sub\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"email_verified\": true\n}\n```","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#oauth-discovery","section":"OAuth Discovery","audience":"api","headings":[],"text":"Typograph implements OAuth 2.0 Authorization Server Metadata ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)):\n\n```bash\ncurl https://api.typograph.nl/.well-known/oauth-authorization-server\n```\n\n**Response:**\n\n```json\n{\n  \"issuer\": \"https://api.typograph.nl\",\n  \"authorization_endpoint\": \"https://api.typograph.nl/oauth/authorize\",\n  \"token_endpoint\": \"https://api.typograph.nl/oauth/token\",\n  \"revocation_endpoint\": \"https://api.typograph.nl/oauth/revoke\",\n  \"introspection_endpoint\": \"https://api.typograph.nl/oauth/introspect\",\n  \"scopes_supported\": [\n    \"openid\", \"profile\", \"email\", \"offline_access\",\n    \"authorization\", \"health\",\n    \"identity\", \"identity:read\", \"identity:write\", \"identity:delete\",\n    \"file\", \"file:read\", \"file:write\", \"file:delete\",\n    \"document\", \"document:read\", \"document:write\",\n    \"publisher\", \"publisher:read\", \"publisher:write\",\n    \"converter\", \"converter:read\", \"converter:write\",\n    \"webhook\", \"webhook:read\", \"webhook:write\", \"webhook:delete\",\n    \"subscription\", \"subscription:read\", \"subscription:write\"\n  ],\n  \"response_types_supported\": [\"code\"],\n  \"response_modes_supported\": [\"query\"],\n  \"grant_types_supported\": [\"authorization_code\", \"client_credentials\", \"refresh_token\"],\n  \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\", \"none\"],\n  \"code_challenge_methods_supported\": [\"S256\"],\n  \"client_id_metadata_document_supported\": true,\n  \"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\", \"none\"],\n  \"introspection_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\"],\n  \"service_documentation\": \"https://docs.typograph.nl\"\n}\n```\n\nThe full scope catalog — including descriptions and the parent/child hierarchy — is documented in [Scopes](https://docs.typograph.nl/guides/api/scopes).","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#client-types","section":"Client Types","audience":"api","headings":["Confidential Clients","Public Clients"],"text":"### Confidential Clients\n\nServer-side applications that can securely store credentials.\n\n- Use Client Credentials or Authorization Code flow\n- Must include `client_secret` in token requests\n- Suitable for: Backend services, server-side web apps\n\n### Public Clients\n\nClient-side applications that cannot securely store credentials.\n\n- Must use Authorization Code flow with PKCE\n- Cannot use `client_secret`\n- Suitable for: SPAs, mobile apps, desktop applications","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#error-handling","section":"Error Handling","audience":"api","headings":["OAuth Errors","HTTP Status Codes","Token Type Errors"],"text":"### OAuth Errors\n\n| Error | Description |\n|-------|-------------|\n| `invalid_request` | Missing or invalid parameter |\n| `invalid_client` | Client authentication failed, or the request comes from an address outside the app's [IP allowlist](https://docs.typograph.nl/guides/api/authentication#ip-allowlist) |\n| `invalid_grant` | Invalid authorization code or refresh token. With `\"reason\": \"sign_in_expired\"`: the sign-in is older than 30 days, and the user signs in again |\n| `unauthorized_client` | Client not authorized for this grant type |\n| `unsupported_grant_type` | Grant type not supported |\n| `token_reuse_grace_period` | A refresh token sent again within a minute; use the tokens from the first response |\n| `token_reuse_security_breach` | A refresh token sent again later; every token of that sign-in is revoked, sign in again |\n| `invalid_scope` | Requested scope is invalid or exceeds granted scopes |\n\n**Error Response:**\n\n```json\n{\n  \"error\": \"invalid_grant\",\n  \"error_description\": \"The authorization code has expired\"\n}\n```\n\n### HTTP Status Codes\n\n| Code | Description |\n|------|-------------|\n| `401 Unauthorized` | Missing or invalid access token |\n| `403 Forbidden` | Token lacks required scope, wrong token type, the request names an organization the token does not reach, or comes from an address outside the app's IP allowlist |\n\n### Token Type Errors\n\n| Error Code | Description |\n|------------|-------------|\n| `invalid_token_type` | Wrong token type for endpoint (e.g., using client token on user-only endpoint) |\n| `insufficient_scope` | Token lacks required OAuth scope |\n| `organization_not_bound` | The request names another organization than the one the token reaches ([details](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)) |\n| `ip_not_allowed` | The token's app has an [IP allowlist](https://docs.typograph.nl/guides/api/authentication#ip-allowlist) and the request comes from another address |","agent_tools":[]},{"title":"Authentication","url":"https://docs.typograph.nl/guides/api/authentication#security-best-practices","section":"Security Best Practices","audience":"api","headings":[],"text":"1. **Store credentials securely** - Never commit secrets to version control\n2. **Use environment variables** - Keep credentials out of your codebase\n3. **Request minimal scopes** - Only request what your application needs\n4. **Use PKCE** - Always use PKCE for Authorization Code flow\n5. **Rotate credentials** - Periodically rotate client secrets\n6. **Limit server apps to their addresses** - An [IP allowlist](https://docs.typograph.nl/guides/api/authentication#ip-allowlist) makes a leaked secret useless elsewhere\n7. **Validate tokens** - Use introspection for sensitive operations\n8. **Handle expiration** - Implement automatic token refresh\n9. **Use HTTPS** - Always use secure connections","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter","section":null,"audience":"api","headings":[],"text":"The Converter service transforms files between different formats. It supports InDesign and POD (Print on Demand) package conversion, PDF conversion, font format conversion, and image processing.\n\n**Base URL:** `/v1/converter/`\n\n**Scopes:** `converter` (full), `converter:read`, `converter:write`\n\n**Token type:** `any` — converter endpoints accept either a user token or a client token. See [Token Types](https://docs.typograph.nl/guides/api/token-types).","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#endpoints","section":"Endpoints","audience":"api","headings":[],"text":"| Method | Path | Scopes | Description |\n|--------|------|--------|-------------|\n| `GET` | `/v1/converter/jobs` | `converter` / `converter:read` | List conversion jobs |\n| `POST` | `/v1/converter/jobs` | `converter` / `converter:write` | Create a conversion job |\n| `GET` | `/v1/converter/jobs/{id}` | `converter` / `converter:read` | Job detail |\n| `GET` | `/v1/converter/jobs/{id}/files` | `converter` / `converter:read` | Download the converted files, see [File Types](https://docs.typograph.nl/guides/api/converter#file-types) |\n| `GET` | `/health/converter` | — | Service health (public, unauthenticated) |","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#supported-conversions","section":"Supported Conversions","audience":"api","headings":["Package Conversion","PDF Conversion","Font Conversion","Image Conversion","Variants for an asset store"],"text":"### Package Conversion\n\n| Input | Output | Description |\n|-------|--------|-------------|\n| `indesign_package` | `typograph_package` | Convert an InDesign package (zipped Collect-for-Output bundle) to Typograph format |\n| `pod_package` | `typograph_package` | Convert a POD (Print on Demand) package to Typograph format |\n\n### PDF Conversion\n\n| Input | Output | Description |\n|-------|--------|-------------|\n| `pdf` | `typograph_package` | Convert a PDF to a template, see [Convert a PDF](https://docs.typograph.nl/guides/api/converter#convert-a-pdf) |\n| `ai` | `typograph_package` | Convert an Illustrator file saved with PDF compatibility to a template |\n\n### Font Conversion\n\n| Input Types | Output Types |\n|-------------|--------------|\n| `eot`, `otf`, `ttf`, `woff`, `woff2`, `ttc` | `ttf`, `otf`, `woff`, `woff2` |\n\n### Image Conversion\n\n| Input Types | Output Types |\n|-------------|--------------|\n| `bmp`, `ai`, `eps`, `gif`, `psd`, `jpg`, `jpeg`, `png`, `svg`, `webp`, `tiff`, `tif` | `jpg`, `png`, `webp`, `svg` |\n\nAn `ai` file converts to `png` or `svg` only: a job asking for `jpg` or `webp` is refused with a `422` (`validation_error`). Its `image_variants` are allowed: that rule is about the output type, not the variants inside the zip. Into a `typograph_package`, an `ai` file is a [PDF conversion](https://docs.typograph.nl/guides/api/converter#convert-a-pdf).\n\n### Variants for an asset store\n\n| Input Types | Output Type |\n|-------------|-------------|\n| A font (`eot`, `otf`, `ttf`, `woff`, `woff2`, `ttc`), or `font_package`: a zip of fonts | `font_variants` |\n| An image (`bmp`, `ai`, `eps`, `gif`, `psd`, `jpg`, `jpeg`, `png`, `svg`, `webp`, `tiff`, `tif`), or `image_package`: a zip of images | `image_variants` |\n\nOne job converts a file, or a whole zip of them, into everything a store serves for it, as one zip with a `manifest.json` (layouts under [Package Formats](https://docs.typograph.nl/guides/api/converter#package-formats)). A zip of many files costs one job instead of one each.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#package-formats","section":"Package Formats","audience":"api","headings":["indesign_package (input)","pod_package (input)","font_package and image_package (input)","image_variants (output)","font_variants (output)","typograph_package (output)"],"text":"Package conversions exchange ZIP archives. The exact archive layout matters — the converter validates and rejects anything it can't parse. The structures below describe what to send in (`input.source.url`) and what to expect back (uploaded to `output.upload.url`).\n\n### `indesign_package` (input)\n\nA standard Adobe InDesign **Collect for Output** bundle, zipped. The archive may contain a single wrapper folder or place the bundle contents at the root — both are accepted (the converter ignores `__MACOSX` and `.DS_Store` entries).\n\n```text\nindesign_package.zip\n└── <bundle-folder>/                  (optional wrapper — may be omitted)\n    ├── <document>.idml               required — the InDesign document\n    ├── Links/                        required if the document references images\n    │   ├── photo-01.jpg\n    │   └── logo.ai\n    └── Document fonts/               required if the document references fonts\n        ├── MyFont-Regular.otf\n        └── MyFont-Bold.otf\n```\n\nLinked images and embedded fonts are converted as part of the job — see the `typograph_package` output below for what comes back.\n\n### `pod_package` (input)\n\nA ZIP containing the MY VIVENDA POD template JSON. The converter scans the archive, skips macOS metadata (`__MACOSX/`, `._*`), and uses the **first** `.json` file it finds.\n\n```text\npod_package.zip\n└── <any-name>.json                   POD template — single template object OR array of templates\n```\n\nThe JSON itself follows the [POD template schema](https://github.com/typograph/typograph-pod-schema). Multiple templates in one array become multiple pages in the resulting Typograph document.\n\n### `font_package` and `image_package` (input)\n\nA zip of fonts, or of images. Every file in it is converted, at any depth of folders, except\nhidden files (a name starting with `.`) and `__MACOSX` entries, so put only fonts, or only images,\nin it:\n\n```\nimage_package.zip\n├── logo.svg\n├── photos/cover.tiff\n└── photos/team.psd\n```\n\n### `image_variants` (output)\n\n```\nimage_variants.zip\n├── images/<name>_<uuid>/original.<ext>   the image as it came in\n├── images/<name>_<uuid>/print.png\n├── images/<name>_<uuid>/editor.webp\n├── images/<name>_<uuid>/thumbnail.webp\n└── manifest.json                         one entry per image\n```\n\n### `font_variants` (output)\n\n```\nfont_variants.zip\n├── fonts/<stem>_<uuid>/original.<ext>    the font as it came in\n├── fonts/<stem>_<uuid>/<stem>.ttf        the TrueType the publisher reads\n└── manifest.json                         one entry per font, with its family and style\n```\n\nA variable font becomes one static font per named instance, so one upload can be several entries.\n\n### `typograph_package` (output)\n\nThe converted bundle. Contents differ slightly between InDesign and POD sources:\n\n```text\ntypograph_package.zip                 (from indesign_package conversion)\n├── template_bundle.json              { \"version\": \"1.0.0\", \"template\": {...}, \"manifest\": {...} }\n├── template_thumbnail.webp           the first page as a picture (see Text Layout)\n├── images/                           browser-ready variants of all linked images\n│   ├── photo-01.webp\n│   └── logo.png\n└── fonts/                            TTF versions of all embedded fonts\n    ├── MyFont-Regular.ttf\n    └── MyFont-Bold.ttf\n\ntypograph_package.zip                 (from pod_package conversion)\n└── template_bundle.json              { \"version\": \"1.0.0\", \"template\": {...}, \"manifest\": {...} }\n\ntypograph_package.zip                 (from pdf or ai conversion)\n├── template_bundle.json              { \"version\": \"1.0.0\", \"template\": {...}, \"manifest\": {...} }\n├── images/                           the images of the PDF\n├── fonts/google/                     the fonts Google Fonts has, in full (see What to expect)\n└── fonts/pdf/                        the other fonts, as previews from the PDF (see What to expect)\n```\n\nThe `template_bundle.json` carries both the **template** (spreads, elements, styles, swatches, `font_data`) and the **manifest** (referenced images and fonts) as top-level fields. See the [Documents concept page](https://docs.typograph.nl/guides/concepts/documents) for the full schema.\n\nA package made before the file was renamed holds it as `templatebundle.json`. Every Typograph reader of a package, from an upload to the portal to the canvas, still takes that name, so an older package keeps working; a new one always carries `template_bundle.json`.\n\n:::tip Feeding the output into Publisher\nThe [Publisher Service](https://docs.typograph.nl/guides/api/publisher) does **not** accept `typograph_package.zip` directly, but it does accept the `template_bundle.json` inside it: unzip the package, host `template_bundle.json`, and pass its URL as `input.bundle.url`. Images and fonts in `images/` and `fonts/` are referenced by URL in the manifest, so they need to stay reachable from the Publisher worker.\n:::","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#create-a-conversion-job","section":"Create a Conversion Job","audience":"api","headings":["Request Structure"],"text":"### Request Structure\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `tag` | string | No | Optional tag to identify or group jobs (max 255 chars) |\n| `input` | object | Yes | Input file specification |\n| `input.type` | string | Yes | Input file format |\n| `input.source` | object | Yes | Source file URL and optional headers |\n| `input.source.url` | string | Yes | URL to download input file |\n| `input.source.headers` | object | No | HTTP headers for downloading (max 5) |\n| `output` | object | Yes | Output specification |\n| `output.type` | string | Yes | Output file format |\n| `output.upload` | object | Yes | Target for converted files |\n| `output.upload.url` | string | Yes | URL to upload converted files |\n| `output.upload.headers` | object | No | HTTP headers for uploading (max 5) |\n| `output.config` | object | No | Image options (see [Image Configuration](https://docs.typograph.nl/guides/api/converter#image-configuration)), or `single_pages` for a `typograph_package` (see [Single pages](https://docs.typograph.nl/guides/api/converter#single-pages)) |\n| `url_headers` | array | No | Header rules for the source download and the upload, at most 10: see [Headers for the URLs of a job](https://docs.typograph.nl/guides/api/url-headers) |","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#convert-an-indesign-package","section":"Convert an InDesign Package","audience":"api","headings":["Response"],"text":"Convert an InDesign package to Typograph format:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"design-project-alpha\",\n    \"input\": {\n      \"type\": \"indesign_package\",\n      \"source\": {\n        \"url\": \"https://example.com/packages/design.zip\"\n      }\n    },\n    \"output\": {\n      \"type\": \"typograph_package\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      }\n    }\n  }'\n```\n\n### Response\n\n```json\n{\n  \"id\": \"01234567-89ab-cdef-0123-456789abcdef\",\n  \"tag\": \"design-project-alpha\",\n  \"status\": \"queued\",\n  \"message\": null,\n  \"warnings\": [],\n  \"input\": {\n    \"type\": \"indesign_package\",\n    \"source\": {\n      \"url\": \"https://example.com/packages/design.zip\"\n    }\n  },\n  \"output\": {\n    \"type\": \"typograph_package\",\n    \"upload\": {\n      \"url\": \"https://example.com/webhook/upload\"\n    }\n  },\n  \"created_at\": \"<ISO 8601 timestamp>\"\n}\n```","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#convert-a-pod-package","section":"Convert a POD Package","audience":"api","headings":[],"text":"Convert a POD (Print on Demand) package — a zipped archive of POD-schema documents — to Typograph format:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"pod-import-batch-001\",\n    \"input\": {\n      \"type\": \"pod_package\",\n      \"source\": {\n        \"url\": \"https://example.com/pod/template.zip\"\n      }\n    },\n    \"output\": {\n      \"type\": \"typograph_package\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      }\n    }\n  }'\n```\n\nThe response shape is the same as for InDesign packages — only `input.type` differs. POD packages always convert to `typograph_package`; pairing `pod_package` with any other output type is rejected with a validation error.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#convert-a-pdf","section":"Convert a PDF","audience":"api","headings":["What to expect","Limits and failures"],"text":"Convert a PDF, or an Illustrator file saved with PDF compatibility, to a template:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"spring-flyer\",\n    \"input\": {\n      \"type\": \"pdf\",\n      \"source\": {\n        \"url\": \"https://example.com/files/spring-flyer.pdf\"\n      }\n    },\n    \"output\": {\n      \"type\": \"typograph_package\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      }\n    }\n  }'\n```\n\nThe response shape is the same as for InDesign packages. For an Illustrator file, set `input.type` to `ai`. A PDF converts to `typograph_package` only: pairing `pdf` with any other output type is refused with a `422` (`validation_error`), and so is `output.config.single_pages`, because every page of a PDF already becomes one page.\n\nThe source URL does not have to end in `.pdf`: the converter checks the file itself, so a presigned URL without a file name works. The template is named after the file name in the URL, or `document` when the URL names none.\n\n### What to expect\n\nA PDF describes how a page looks, not how it was built, so a converted PDF is a **starting point**: the layout, the text as editable text, the images and the colours are carried over, and the template needs finishing rather than rebuilding. Check the result before printing it.\n\n| Carried over | Not carried over (yet) |\n|--------------|------------------------|\n| Every page, as one page of the template | Gradients and patterns (`pdf_gradient`) |\n| Text, as editable text boxes in their own lines | Spot colours (`pdf_spot_color`) |\n| Photos and other images, with their transparency: a soft shadow or a blurred shape stays see-through | Some image formats and colour spaces, such as CMYK pixels that are not a JPEG, and 16-bit images (`pdf_image_unsupported`) |\n| Vector shapes with a solid colour | The transparency of a CMYK JPEG (the JPEG itself is carried over), or of an image whose transparency cannot be read (`pdf_image_transparency_lost`) |\n| The order of what lies on top, and how shapes and images blend with what lies under them (multiply, overlay, lighten and the other blend modes) | A scanned page has no text: it comes out as an image |\n\nThe conversion is built and tuned on PDFs exported from Canva. A PDF from another program converts as far as the converter reads it, and **every item it could not carry over is a warning** on the job (below), so nothing is dropped without saying so. For the best result:\n\n- export a PDF rather than flattened print output, and without a password;\n- keep text as text, not as outlines;\n- save an Illustrator file with **Create PDF Compatible File** on.\n\nA PDF only holds its fonts cut down to the letters the design uses. So **every font that [Google Fonts](https://fonts.google.com) has comes along in full**: its file is in the package under `fonts/google/`, its entry in the manifest has `\"source\": \"google\"`, and the job has a `font_added` warning for it (below). Google Fonts publishes its fonts under open licences, most of them the SIL Open Font License; the licence of a family is on its page at Google Fonts.\n\nAny other font comes along as a **preview** when the PDF embeds it: the cut-down copy, as a TrueType file under `fonts/pdf/`, its manifest entry with `\"source\": \"pdf\"` and the letters it has in `available_characters`, and a `font_preview` warning. A preview shows the design as it was, but it has only the letters the PDF uses, and it is not a font to edit or print with: upload the complete font and use that. Typograph imports such a font as a preview of its template and asks for the complete one ([Template uploads](https://docs.typograph.nl/guides/api/file-service#template-uploads)). A font is not taken from the PDF when its owner does not allow it to be embedded, or when the copy cannot be read as a font a browser can show.\n\nA font that comes along neither way is named by the template by family and style only, without a file; text in it is shown in the default font, with a `font_missing` warning. Add the complete fonts to the project before editing or printing the template. When Google Fonts cannot be reached, the job still completes, without those fonts.\n\n### Limits and failures\n\nA PDF conversion takes a file of at most **100 MB**. A larger file fails the job with `The input file could not be downloaded.`, like any download that fails.\n\nWhen the file cannot be converted, the job fails (`status` `failed`, nothing uploaded) and `message` says why, in a sentence to show as it is:\n\n| `message` | What to do |\n|-----------|------------|\n| `The file is not a PDF.` | Send a PDF |\n| `The Illustrator file was not saved with PDF compatibility. Save it with \"Create PDF Compatible File\" on, or export it as a PDF.` | Save the `.ai` file again as it says |\n| `The PDF is protected by a password. Upload it without one.` | Save the PDF without a password |\n| `The PDF could not be read.` | Export the PDF again from the program that made it |\n| `The PDF could not be read within 120 seconds.` | Export a simpler PDF, or split it |","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#warnings","section":"Warnings","audience":"api","headings":[],"text":"Every converter job, whatever it converts, has `warnings`: what the conversion could not carry\nover, one entry per item. Every entry has a `code` to act on and a `message` to show as it is, and\nmay carry data of its own:\n\n```json\n\"warnings\": [\n  {\"code\": \"pdf_gradient\", \"message\": \"Page 1: a shape could not be converted (it is a colour gradient or a pattern).\", \"page\": 1}\n]\n```\n\nIt is always a list, empty when there is nothing to report, in `GET /v1/converter/jobs/{id}` and in\nthe `conversion.completed` event. Read a warning by its `code`, and ignore a code you do not know:\nmore are added as conversions learn to report them. `message` keeps its meaning: why a job failed,\nor a note about the whole job (see [Text Layout](https://docs.typograph.nl/guides/api/converter#text-layout)).\n\nA PDF conversion fills `warnings` with what it could not carry over, with the page it is on in\n`page` (counted from 1). Shapes with the same problem on one page are one warning, with the count\nin `message`. A job keeps at most 10 warnings per code; the last of them then has `more`, the\nnumber of warnings with that code that were left out.\n\n| `code` | Meaning |\n|--------|---------|\n| `pdf_image_unsupported` | An image in a format, compression or colour space that is not read (CMYK pixels that are not a JPEG, 16-bit); the reason is in `message` |\n| `pdf_image_damaged` | An image whose pixel data is broken |\n| `pdf_image_uncropped` | An image cropped by more than one outline at once, placed uncropped |\n| `pdf_image_transparency_lost` | A partly transparent image whose transparency could not be kept (a CMYK JPEG, or a mask that cannot be read), placed without it |\n| `pdf_gradient` | Shapes filled with a gradient or a pattern, not carried over |\n| `pdf_spot_color` | Shapes in a spot colour, not carried over |\n| `pdf_shape_unsupported` | Shapes not carried over for another reason; the reason is in `message` |\n| `pdf_shape_uncropped` | Shapes cropped in a way that could not be kept, shown whole |\n| `pdf_font_default` | Text in a font the PDF gives no name (emoji, some text effects), shown in the default font |\n\nA PDF conversion also adds a `font_added` for every font it took from Google Fonts, with the font in `name` as a person reads it (`Montserrat Bold`, or `Anton` for a regular style) and where it came from in `source`:\n\n```json\n{\"code\": \"font_added\", \"message\": \"The font \\\"Montserrat Bold\\\" was added from Google Fonts.\", \"source\": \"google\", \"name\": \"Montserrat Bold\"}\n```\n\nA `font_preview` for every font it shows from the PDF's own copy, with `source` `pdf`, and a `font_missing` for every font whose text is in the default font, each with the font in `name`:\n\n```json\n{\"code\": \"font_preview\", \"message\": \"The font \\\"Helvetica World Bold\\\" is shown as a preview from the PDF, with only the characters the PDF uses. Upload the complete font to edit the text.\", \"source\": \"pdf\", \"name\": \"Helvetica World Bold\"}\n{\"code\": \"font_missing\", \"message\": \"The font \\\"Lust Didone\\\" is not in the package: its text is shown in the default font until the font is uploaded.\", \"name\": \"Lust Didone\"}\n```\n\nOther conversions do not fill `warnings` yet.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#text-layout","section":"Text Layout","audience":"api","headings":["Single pages"],"text":"A converted document carries its text, its styles and its frames, but an InDesign source does not say where each line breaks or where each word sits. Typograph works that out — the **text layout** — with the same engine the editor uses, as the last step of every InDesign package conversion. The `typograph_package` that comes back has its text laid out, so it can go to the [Publisher Service](https://docs.typograph.nl/guides/api/publisher) without being opened in the editor first.\n\n:::note POD packages are not laid out\nA POD document names font *slots* rather than fonts, and the real fonts are set later. Its text cannot be laid out until they are, so a `pod_package` conversion comes back without text layout, as before, with its pages as they are. Asking a `pod_package` job for single pages (`true` or `\"keep_together\"`) is refused with `422`.\n:::\n\nThe same step draws the first page as `template_thumbnail.webp`, at most 1600 pixels on its longest side, next to `template_bundle.json`. Show it as a preview of the template. A package uploaded to Typograph uses it as the template's thumbnail until the template is first saved in the editor. A package whose text could not be laid out comes back without one.\n\nThe layout uses the fonts in the package. **If a font the document uses is missing from the package or cannot be read, the text is not laid out at all**, rather than laid out in a substitute font that would move every line break. The job still completes, and `message` says why and names the fonts. The pages are still split as `single_pages` asks (below): that does not depend on the text.\n\n| `message` on a completed job | Meaning | What to do |\n|------------------------------|---------|------------|\n| `null` | The text is laid out | Nothing |\n| `Converted without text layout: these fonts did not load: \"Gotham\" (normal), \"Gotham\" (bold). Open it in the editor once before printing.` | These fonts are missing or unreadable. At most five are named, then `and n more`; a font known only by its file is named by the file | Add the fonts to the InDesign package (`Document fonts/`) and convert again, or open the template in the editor once, with the fonts available, before printing |\n| `Converted without text layout: the layout service did not answer. Open it in the editor once before printing.` | The layout step was unavailable | Convert again later, or open the template in the editor once before printing |\n\nWhen the layout service did not answer, no pages are split either: unless the job asked for `\"single_pages\": false` (below), that message ends with ` single_pages was not applied: the template keeps its spreads.`\n\nIf the converted document cannot be opened at all, so that no page can be built from it, **the job fails** rather than deliver a blank template: `status` is `failed`, nothing is uploaded, and `message` is `The converted template could not be opened: its pages could not be built from the converted document, so it was not uploaded.` The `conversion.failed` event carries it in `message` and in `error`, with `error_code` `document_not_built` (see [Webhooks](https://docs.typograph.nl/guides/api/webhooks#conversioncompleted--conversionfailed)). Such a template would also open blank in the editor: write to [support@typograph.nl](mailto:support@typograph.nl) with the job id and the InDesign package.\n\n:::caution A missing font is not always caught yet\nA font family that is missing from the package **entirely** is not always detected: such a template can come back laid out in a substitute face, with `message` empty. Until that is fixed, check InDesign's package report for missing fonts before converting, and look at the converted template before printing it.\n:::\n\n:::tip Fonts from Adobe Fonts are not packaged\nInDesign's **File ▸ Package** does not copy fonts activated through Adobe Fonts (Creative Cloud) into `Document fonts/`; the package report lists them as not included. That is the most common reason a font is missing. Use a font file you can package, or add it to `Document fonts/` yourself.\n:::\n\n:::info Check `message` on completed jobs too\n`message` is no longer only for failed jobs. A package conversion can complete **without** text layout; check `message` on the job (or the `conversion.completed` event) before sending the template to print.\n:::\n\n### Single pages\n\nBy default a converted InDesign template comes back as **single pages**, except the spreads the document keeps together. Set `output.config.single_pages` to choose:\n\n| `single_pages` | Result |\n|----------------|--------|\n| `\"keep_together\"` (default) | Single pages, except the spreads the document keeps together: InDesign spreads that do not allow their pages to shuffle (an island spread such as a gatefold or a centrefold), and a document that opens on a two-page spread |\n| `true` | Every spread split into single pages |\n| `false` | Spreads as in the source document: a two-page InDesign spread stays one spread |\n\nTo keep the spreads:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"brochure-spreads\",\n    \"input\": {\n      \"type\": \"indesign_package\",\n      \"source\": {\n        \"url\": \"https://example.com/packages/brochure.zip\"\n      }\n    },\n    \"output\": {\n      \"type\": \"typograph_package\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      },\n      \"config\": {\n        \"single_pages\": false\n      }\n    }\n  }'\n```\n\n`single_pages` applies to a `typograph_package` output only; with any other output type the request is refused with `422` (`single_pages can only be set when the output type is typograph_package.`). Like the image options, it is not repeated in job responses.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#convert-fonts","section":"Convert Fonts","audience":"api","headings":[],"text":"Convert a TTF font to WOFF2 for web use:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"font-conversion-001\",\n    \"input\": {\n      \"type\": \"ttf\",\n      \"source\": {\n        \"url\": \"https://example.com/fonts/myfont.ttf\"\n      }\n    },\n    \"output\": {\n      \"type\": \"woff2\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      }\n    }\n  }'\n```","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#convert-and-resize-images","section":"Convert and Resize Images","audience":"api","headings":["Image Configuration","Fit Options"],"text":"Convert images with optional resizing and optimization:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"image-optimization\",\n    \"input\": {\n      \"type\": \"png\",\n      \"source\": {\n        \"url\": \"https://example.com/images/photo.png\"\n      }\n    },\n    \"output\": {\n      \"type\": \"webp\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      },\n      \"config\": {\n        \"width\": 1200,\n        \"height\": 800,\n        \"quality\": 85,\n        \"fit\": \"max\",\n        \"strip\": true\n      }\n    }\n  }'\n```\n\n### Image Configuration\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `width` | integer | Target width in pixels (1-10000) |\n| `height` | integer | Target height in pixels (1-10000) |\n| `quality` | integer | Output quality (1-100, default: 90) |\n| `fit` | string | Sizing method (see below) |\n| `strip` | boolean | Remove metadata from output |\n\n### Fit Options\n\n| Value | Description |\n|-------|-------------|\n| `max` | Scale down to fit within the dimensions, maintaining aspect ratio. An image already smaller than the box keeps its own size: `max` never enlarges |\n| `min` | Scale to cover the dimensions, maintaining aspect ratio, so one side may come out larger than asked. Enlarges an image smaller than the box |\n| `exact` | Stretch to exactly `width` × `height`, ignoring the aspect ratio. Needs both. Enlarges an image smaller than the box |\n| `crop` | Scale to cover the dimensions, then cut the centre to exactly `width` × `height`. Needs both. Enlarges an image smaller than the box |\n\n`exact` and `crop` without both `width` and `height` are refused with a `422` (`validation_error`). A vector image (SVG, AI, EPS) has no pixel size of its own. Rendered to PNG, every mode scales it to the box. Converted to JPG or WEBP (SVG and EPS only: an AI is refused, see [Image Conversion](https://docs.typograph.nl/guides/api/converter#image-conversion)), it is first rasterised at its own size and then sized like any other image, so `max` does not enlarge it beyond that size. `min` needs a size, and an EPS has none until it is rendered: an EPS to JPG or WEBP with `fit` `min` is refused with a `422` (`validation_error`). Convert it to PNG, or choose another fit.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#download-converted-files","section":"Download Converted Files","audience":"api","headings":["File Types"],"text":"Download the output once conversion is complete:\n\n```bash\ncurl \"https://api.typograph.nl/v1/converter/jobs/{id}/files?file_type=output\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -o converted-file.zip\n```\n\n### File Types\n\n| Type | Description |\n|------|-------------|\n| `output` | Converted file (default) |\n| `input` | Original source file |","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#upload-request-format","section":"Upload Request Format","audience":"api","headings":["Uploading Directly to Cloud Storage"],"text":"When a job completes successfully, Typograph sends the converted file as a **single HTTP PUT request** to `output.upload.url`. Your upload target must accept this request shape:\n\n| Property | Value |\n|----------|-------|\n| Method | `PUT` |\n| URL | Exactly the URL you provided in `output.upload.url` (redirects are not followed) |\n| `Content-Type` | Detected from the file — e.g. `font/woff2`, `image/webp`, `image/png`, `application/zip` for package conversions |\n| `User-Agent` | `Typograph/Util-Upload` |\n| Custom headers | Merged from `output.upload.headers`, or else from the [`url_headers`](https://docs.typograph.nl/guides/api/url-headers) rule that covers the upload URL (can't override `Content-Type` or `User-Agent`) |\n| Body | Raw file bytes, streamed (no multipart / form-data wrapper) |\n| Connection timeout | 10 seconds |\n| Transfer timeout | 300 seconds |\n\nYour endpoint must respond with an HTTP status in the **200–299** range — anything else fails the job.\n\n### Uploading Directly to Cloud Storage\n\nYou can point `output.upload.url` straight at a presigned URL from S3, GCS, Azure Blob, Cloudflare R2, etc. Each provider accepts signed PUT requests with a raw body; set `output.upload.headers` to whatever the signer expects. Make sure the presigned URL is signed against the content type Typograph will send (see the list above) — protected headers (`Content-Type`, `User-Agent`) cannot be overridden.","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#custom-headers","section":"Custom Headers","audience":"api","headings":[],"text":"Give a URL that needs a header its own `headers`, as below, or give the job [`url_headers`](https://docs.typograph.nl/guides/api/url-headers): rules that send their headers with every request to a URL they cover, for both the source download and the upload.\n\n```json\n{\n  \"url_headers\": [\n    {\n      \"match\": \"https://storage.example.com/private/\",\n      \"headers\": { \"Authorization\": \"Bearer storage-token\" }\n    }\n  ],\n  \"input\": {\n    \"type\": \"png\",\n    \"source\": { \"url\": \"https://storage.example.com/private/image.png\" }\n  },\n  \"output\": {\n    \"type\": \"webp\",\n    \"upload\": { \"url\": \"https://webhook.example.com/uploads\" }\n  }\n}\n```\n\nThe per-URL form, an exact rule that wins over any prefix:\n\n```json\n{\n  \"tag\": \"protected-conversion\",\n  \"input\": {\n    \"type\": \"png\",\n    \"source\": {\n      \"url\": \"https://storage.example.com/private/image.png\",\n      \"headers\": {\n        \"Authorization\": \"Bearer storage-token\"\n      }\n    }\n  },\n  \"output\": {\n    \"type\": \"webp\",\n    \"upload\": {\n      \"url\": \"https://webhook.example.com/uploads\",\n      \"headers\": {\n        \"X-API-Key\": \"webhook-api-key\"\n      }\n    }\n  }\n}\n```","agent_tools":[]},{"title":"Converter Service","url":"https://docs.typograph.nl/guides/api/converter#examples","section":"Examples","audience":"api","headings":["PSD to PNG Thumbnail","AI to SVG Conversion","Batch Font Conversion","Complete Example with Headers"],"text":"### PSD to PNG Thumbnail\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"psd-thumbnail\",\n    \"input\": {\n      \"type\": \"psd\",\n      \"source\": {\n        \"url\": \"https://example.com/designs/layout.psd\"\n      }\n    },\n    \"output\": {\n      \"type\": \"png\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      },\n      \"config\": {\n        \"width\": 400,\n        \"fit\": \"max\"\n      }\n    }\n  }'\n```\n\n### AI to SVG Conversion\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"logo-conversion\",\n    \"input\": {\n      \"type\": \"ai\",\n      \"source\": {\n        \"url\": \"https://example.com/logos/company-logo.ai\"\n      }\n    },\n    \"output\": {\n      \"type\": \"svg\",\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      }\n    }\n  }'\n```\n\n### Batch Font Conversion\n\nConvert multiple fonts by creating separate jobs:\n\n```bash\nfor font in regular bold italic; do\n  curl -X POST https://api.typograph.nl/v1/converter/jobs \\\n    -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\n      \\\"tag\\\": \\\"font-${font}\\\",\n      \\\"input\\\": {\n        \\\"type\\\": \\\"otf\\\",\n        \\\"source\\\": {\n          \\\"url\\\": \\\"https://example.com/fonts/myfont-${font}.otf\\\"\n        }\n      },\n      \\\"output\\\": {\n        \\\"type\\\": \\\"woff2\\\",\n        \\\"upload\\\": {\n          \\\"url\\\": \\\"https://example.com/webhook/upload\\\"\n        }\n      }\n    }\"\ndone\n```\n\n### Complete Example with Headers\n\nConvert an image from protected storage with custom headers:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/converter/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"premium-image-conversion\",\n    \"input\": {\n      \"type\": \"tiff\",\n      \"source\": {\n        \"url\": \"https://storage.example.com/originals/photo.tiff\",\n        \"headers\": {\n          \"Authorization\": \"Bearer storage-token\"\n        }\n      }\n    },\n    \"output\": {\n      \"type\": \"webp\",\n      \"upload\": {\n        \"url\": \"https://cdn.example.com/upload\",\n        \"headers\": {\n          \"X-Upload-Token\": \"upload-secret-key\"\n        }\n      },\n      \"config\": {\n        \"width\": 1920,\n        \"height\": 1080,\n        \"quality\": 90,\n        \"fit\": \"max\",\n        \"strip\": true\n      }\n    }\n  }'\n```","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document","section":null,"audience":"api","headings":[],"text":"The Document service validates Typograph templates against the schema and upgrades older templates to the current schema version. Both endpoints operate on a template that is hosted at a URL — the service downloads it, processes it, and returns the result inline.","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#endpoints","section":"Endpoints","audience":"api","headings":[],"text":"| Method | Path | Description | Token Type | Scopes |\n|--------|------|-------------|------------|--------|\n| `POST` | `/v1/document/validate` | Validate a hosted template against a schema version | user or client | `document`, `document:read` |\n| `POST` | `/v1/document/upgrade` | Upgrade a hosted template to a newer schema version | user or client | `document`, `document:write` |\n| `GET`  | `/health/document` | Liveness check proxied through the gateway | — | — |\n\nBoth operational endpoints accept **either** a user token (Authorization Code / Refresh Token) or a client token (Client Credentials). See [Token Types](https://docs.typograph.nl/guides/api/token-types).","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#request-body","section":"Request Body","audience":"api","headings":["A template behind authentication"],"text":"Both endpoints share the same request body shape:\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `template` | object | Yes | Where the template is downloaded from |\n| `template.url` | string (URL, max 2000 chars) | Yes | URL of the template JSON file. Only `.json` extensions are accepted. `/validate` takes a template or a [template bundle](https://docs.typograph.nl/guides/api/document#a-saved-template); `/upgrade` takes a template. |\n| `template.headers` | object | No | HTTP headers for the download, for example an `Authorization` header (max 5) |\n| `version` | string | No | Schema version (e.g. `\"1.0.0\"`), matching `<major>.<minor>.<patch>`. For `/validate`, the version to validate against; when omitted, the version declared inside the template. For `/upgrade`, the version to upgrade to; when omitted, the latest schema version. |\n\nThe service downloads the template once, within 30 seconds, and the file must be ≤ 50 MB.\n\n### A template behind authentication\n\nWhen the template URL needs a header, give it in `template.headers`:\n\n```json\n{\n  \"template\": {\n    \"url\": \"https://storage.example.com/private/my-template.json\",\n    \"headers\": { \"Authorization\": \"Bearer your-storage-token\" }\n  },\n  \"version\": \"1.0.0\"\n}\n```\n\nThe headers follow the same rules as the headers of a publisher or converter job (see [Headers for the URLs of a job](https://docs.typograph.nl/guides/api/url-headers#rules-for-a-valid-url_headers)):\n\n- at most 5 headers;\n- only allowed names, such as `Authorization`, `X-Api-Key` or any other `X-…` name; `Host`, `Cookie` and the forwarding headers are always refused;\n- a value of at most 8 KB, without CR, LF or NUL.\n\nA header that breaks a rule is a `400` validation error on `template.headers`. The headers are used for this one download only: they are never stored, and never returned.\n\nA presigned URL needs no headers. Don't add an `Authorization` header to one: object stores refuse a request that carries both.","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#validate-a-template","section":"Validate a Template","audience":"api","headings":["A saved template","Response — Valid Template","Response — Validation Errors","Violation Object"],"text":"Check whether a template conforms to the Typograph schema.\n\n### A saved template\n\nThe editor saves a template as a **bundle**: the template and its manifest in one document, `{ \"version\": \"1.0.0\", \"manifest\": {...}, \"template\": {...} }`. That is the file the [File Service](https://docs.typograph.nl/guides/api/file-service) gives you at `GET /v1/file/templates/{id}/file/download-url`, and the `template_bundle.json` the [Converter Service](https://docs.typograph.nl/guides/api/converter#typograph_package-output) produces. Give `/validate` that URL as it is: a document with a `template` object is validated as a bundle, its manifest and its template both, and a violation's `property_path` then starts at the bundle, as in `template.spreads[0]` or `manifest.fonts[1]`. `version` is the bundle's schema version.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/document/validate \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template\": { \"url\": \"https://example.com/my-template.json\" },\n    \"version\": \"1.0.0\"\n  }'\n```\n\n### Response — Valid Template\n\nIf the template is valid, `violations` is empty:\n\n```json\n{\n  \"violations\": [],\n  \"has_violations\": false\n}\n```\n\n### Response — Validation Errors\n\nIf the template has violations, `violations` lists them:\n\n```json\n{\n  \"violations\": [\n    {\n      \"message\": \"This value should be greater than 0.\",\n      \"property_path\": \"spreads[0].elements[2].width\",\n      \"invalid_value\": -10\n    },\n    {\n      \"message\": \"This value should not be null.\",\n      \"property_path\": \"metadata\",\n      \"invalid_value\": null\n    }\n  ],\n  \"has_violations\": true\n}\n```\n\n### Violation Object\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `property_path` | string | JSON-path style pointer to the invalid property inside the document you gave: the template, or the bundle |\n| `message` | string | Human-readable description of the violation |\n| `invalid_value` | any | The value that failed validation |\n\n:::info\nA `200` answers whether the template is valid or not: read `has_violations`, or check that `violations` is empty. A `400` means the template could not be checked at all.\n:::","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#upgrade-a-template","section":"Upgrade a Template","audience":"api","headings":["Response"],"text":"Upgrade an older template to a target schema version. The response is the upgraded template JSON.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/document/upgrade \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template\": { \"url\": \"https://example.com/legacy-template.json\" },\n    \"version\": \"1.0.0\"\n  }'\n```\n\n### Response\n\nThe upgraded template is returned inline, matching the target schema version, and always in the\ncurrent key names: a template saved with the older names (`meta_data`, `page_name`, `story_data`, …)\nis read and comes back renamed (see [Documents](https://docs.typograph.nl/guides/concepts/documents#older-key-names)):\n\n```json\n{\n  \"version\": \"1.0.0\",\n  \"type\": \"template\",\n  \"metadata\": { ... },\n  \"font_data\": [ ... ],\n  \"swatches\": [ ... ],\n  \"spreads\": [ ... ],\n  \"active_spread_index\": 0\n}\n```\n\nSee the [Documents concept page](https://docs.typograph.nl/guides/concepts/documents) for the full template shape.","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#schema-versions","section":"Schema Versions","audience":"api","headings":[],"text":"The current shipping schema version is **`1.0.0`**. The service validates the `version` parameter against the `<major>.<minor>.<patch>` pattern; pass a matching version string, or omit the field: `/validate` then uses the version declared inside the template, and `/upgrade` upgrades to the latest schema version.","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#errors","section":"Errors","audience":"api","headings":[],"text":"| HTTP | Error | Cause |\n|------|-------|-------|\n| `400` | Bad Request | `template.url` missing, not a URL, or not a `.json` file |\n| `400` | Bad Request | `template.headers` has more than 5 headers, a name that is not allowed, or a value with CR or LF |\n| `400` | Bad Request | `version` provided but doesn't match `X.Y.Z` pattern |\n| `400` | Bad Request | `The template could not be downloaded.`: the URL did not answer, answered with an error status, or took longer than 30 seconds |\n| `400` | Bad Request | `The template is larger than the limit of 50MB.`: the template is over the size limit |\n| `401` | Unauthorized | Missing or invalid access token |\n| `403` | Forbidden | Token lacks `document` scope (or `document:read` for validate / `document:write` for upgrade) |\n\nAll errors follow the standard gateway JSON error format (see [API Overview](https://docs.typograph.nl/guides/api/overview)).\n\nA failed download answers with the same message, `The template could not be downloaded.`, whatever the reason, with one exception: a template over the size limit answers `The template is larger than the limit of 50MB.`, because that is something you can fix yourself. The reason (refused connection, timeout, status code) is logged on our side but not returned: it would tell a caller what the service can reach. Errors about the template's *content*, such as invalid JSON or an unsupported version, do come back in full.","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#use-cases","section":"Use Cases","audience":"api","headings":["Pre-Submission Validation","Batch Upgrade"],"text":"### Pre-Submission Validation\n\nValidate a template before handing it off to the Publisher service:\n\n```javascript\nasync function generatePdf(templateUrl, token) {\n  const validation = await fetch('https://api.typograph.nl/v1/document/validate', {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${token}`,\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({ template: { url: templateUrl }, version: '1.0.0' })\n  }).then(r => r.json());\n\n  if (validation.has_violations) {\n    throw new Error(`Template invalid: ${validation.violations.length} violation(s)`);\n  }\n\n  return fetch('https://api.typograph.nl/v1/publisher/jobs', {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${token}`,\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({\n      template: { url: templateUrl },\n      generator: { type: 'pdf' }\n    })\n  }).then(r => r.json());\n}\n```\n\n### Batch Upgrade\n\nUpgrade a list of hosted templates to the current schema version:\n\n```javascript\nasync function upgradeAll(templateUrls, token) {\n  const results = [];\n\n  for (const url of templateUrls) {\n    const upgraded = await fetch('https://api.typograph.nl/v1/document/upgrade', {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${token}`,\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({ template: { url }, version: '1.0.0' })\n    }).then(r => r.json());\n\n    results.push({ source: url, upgraded });\n  }\n\n  return results;\n}\n```","agent_tools":[]},{"title":"Document Service","url":"https://docs.typograph.nl/guides/api/document#related","section":"Related","audience":"api","headings":[],"text":"- [Documents concept page](https://docs.typograph.nl/guides/concepts/documents) — top-level template shape\n- [Pages](https://docs.typograph.nl/guides/concepts/pages) / [Elements](https://docs.typograph.nl/guides/concepts/elements) / [Styles](https://docs.typograph.nl/guides/concepts/styles)\n- [Publisher Service](https://docs.typograph.nl/guides/api/publisher) — consumes validated templates\n- [Token Types](https://docs.typograph.nl/guides/api/token-types) — user vs client token decision","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service","section":null,"audience":"api","headings":[],"text":"The File Service owns all collaborative resources in Typograph: teams, projects, templates, media and fonts, plus sharing, invitations, comments, favorites, trash, and search. The gateway routes everything under `/v1/file/*`.\n\n**Base URL:** `/v1/file/`\n\n**Scopes:** `file` (full), `file:read`, `file:write`, `file:delete`. File Service endpoints require a **user token**, except the reads an integration needs — the teams of its organization, projects, templates, forms and the read-URL endpoints — which also accept a client token; [Token Types](https://docs.typograph.nl/guides/api/token-types#endpoints-accepting-either-token-type-any) lists them.\n\n:::note Path structure\nFile Service paths follow a **\"list-by-parent, item-by-id\"** pattern. Collections are listed and created under their owning parent (projects under teams, templates under projects, media/uploads/fonts under projects), but individual resources are addressed by their own top-level ID once created. Top-level `GET /v1/file/teams` is an exception — it lists the user's teams in the organization the token reaches ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)).\n:::","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#teams","section":"Teams","audience":"api","headings":["Names","The plan's number of teams"],"text":"| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/teams` | GET | `file` / `file:read` |\n| `/v1/file/teams` | POST | `file` / `file:write` |\n| `/v1/file/teams/{id}` | GET | `file` / `file:read` |\n| `/v1/file/teams/{id}` | PATCH | `file` / `file:write` |\n| `/v1/file/teams/{id}` | DELETE | `file` / `file:delete` |\n| `/v1/file/teams/{teamId}/contacts` | GET | `file` / `file:read` |\n| `/v1/file/organizations/{organizationId}/teams` | GET | `file` / `file:read` |\n\n`GET /organizations/{organizationId}/teams` lists the teams of the organization the token reaches ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)); any other organization is a `403`. It answers `{ \"data\": [...], \"meta\": { \"total\", \"limit\", \"offset\", \"has_more\" } }` and accepts client tokens too.\n\n```bash\ncurl https://api.typograph.nl/v1/file/teams \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nList responses follow standard [pagination](https://docs.typograph.nl/guides/api/pagination).\n\nEvery team names the organization it belongs to: `organization_id`, and `organization_name`. `organization_name` is `null` for an organization whose name has not reached the file service yet. Through the API that is the organization the token reaches, with one exception: where another organization lets the apps of its guests read ([Apps of guests](https://docs.typograph.nl/guides/api/identity-service#apps-of-guests)), the teams the user has a role on there are listed too, for reading only. In the portal, someone invited to a team of an organization they are not a member of sees that team too, under its organization.\n\n```json\n{\n  \"id\": \"019b...\",\n  \"name\": \"Brand refresh\",\n  \"organization_id\": \"019a...\",\n  \"organization_name\": \"Studio Noord\",\n  \"creator_id\": \"019a...\",\n  \"metadata\": { \"user_count\": 6, \"admins\": [] },\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"updated_at\": \"<ISO 8601 timestamp>\",\n  \"deleted_at\": null\n}\n```\n\n`DELETE /v1/file/teams/{id}` moves a team to the trash (`?action=delete`, the default) together with its projects and everything in them; `?action=undelete` restores it and what went with it. Both return `204`; restoring a team that is not in the trash is a `400`, and deleting one that is already there a `409` with `in_trash` (see [Nothing works on what is in the trash](https://docs.typograph.nl/guides/api/file-service#nothing-works-on-what-is-in-the-trash)). A trashed team is permanently removed by the scheduled deletion job once the retention window has passed.\n\n### Names\n\nThe `name` of a team or project is 3 to 255 characters, when it is created (`POST /v1/file/teams`, `POST /v1/file/teams/{teamId}/projects`) and when it is renamed (`PATCH`). A name that is missing on create, blank, or outside that range is refused with `400` and `validation_error`, and nothing is created or changed:\n\n```json\n{\n  \"error\": \"validation_error\",\n  \"error_description\": \"Validation errors:\\nname: This value is too short. It should have 3 characters or more..\"\n}\n```\n\nOn create, `name` is the only field the request may hold. The id, the creator, the dates (`created_at`, `updated_at`), the trash (`deleted_at`) and the `metadata` are set by the server; a request that sends one of them, or any other field, is refused with `400` and `invalid_request`, and nothing is created. A rename (`PATCH`) likewise takes `name` alone and refuses any other field the same way.\n\n### The plan's number of teams\n\nAn organization's plan can limit its teams: `max_teams` in the plan's `features` ([`GET /v1/subscription/me`](https://docs.typograph.nl/api-reference)). Free allows one team; Pro and Enterprise have no limit. A team in the trash does not count, but restoring it does. Beyond the limit, `POST /v1/file/teams` and `DELETE /v1/file/teams/{id}?action=undelete` answer `409` with `team_limit_reached`, and a message the user can act on:\n\n```json\n{\n  \"error\": \"team_limit_reached\",\n  \"error_description\": \"Free allows one team. Request another plan, or delete a team first.\"\n}\n```\n\nTeams that exist already stay, also beyond the limit. When the plan cannot be checked, both answer `503` with `subscription_service_unavailable` and change nothing: try again in a moment. `GET /v1/subscription/usage` shows the teams in use against the limit, in `plan_usage.teams`.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#projects","section":"Projects","audience":"api","headings":["Media tags"],"text":"Projects are **team-scoped** — there's no top-level \"list all projects\" endpoint. Create and list projects on a specific team; once you have a project ID you can read, update, and delete it directly.\n\nA project's name follows the same rule as a team's: see [Names](https://docs.typograph.nl/guides/api/file-service#names).\n\n`GET /v1/file/teams/{teamId}/projects` lists the last edited project first, by `updated_at`. A project's `updated_at` moves when it is renamed or moved, and when one of its templates is created, saved, renamed or moved into it; opening a template moves neither. Projects edited at the same moment keep a fixed order; an edit while you page with `limit` and `offset` can still move a project to the front, so a later page may repeat or skip one (see [Pagination](https://docs.typograph.nl/guides/api/pagination)).\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/teams/{teamId}/projects` | GET | `file` / `file:read` |\n| `/v1/file/teams/{teamId}/projects` | POST | `file` / `file:write` |\n| `/v1/file/projects/{id}` | GET | `file` / `file:read` |\n| `/v1/file/projects/{id}` | PATCH | `file` / `file:write` |\n| `/v1/file/projects/{id}` | DELETE | `file` / `file:delete` |\n| `/v1/file/projects/{id}/move` | PATCH | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/templates` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/templates` | POST | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/templates/presigned-urls` | POST | `file` / `file:read` (app keys too) |\n| `/v1/file/projects/{projectId}/uploads` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/uploads` | POST | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/uploads/{id}` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/uploads/{id}` | PATCH | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/media` | GET | `file` / `file:read` / `media` / `media:read` |\n| `/v1/file/projects/{projectId}/media/{id}` | PATCH | `file` / `file:write` / `media` / `media:write` |\n| `/v1/file/projects/{projectId}/media/tags` | POST | `file` / `file:write` / `media` / `media:write` |\n| `/v1/file/projects/{projectId}/tags` | GET | `file` / `file:read` / `media` / `media:read` |\n| `/v1/file/projects/{projectId}/tags/{id}` | PATCH | `file` / `file:write` / `media` / `media:write` |\n| `/v1/file/projects/{projectId}/tags/{id}` | DELETE | `file` / `file:write` / `file:delete` / `media` / `media:write` / `media:delete` |\n| `/v1/file/projects/{projectId}/media/{id}` | DELETE | `file` / `file:write` / `file:delete` / `media` / `media:write` / `media:delete` |\n| `/v1/file/projects/{projectId}/media/presigned-urls` | POST | `file` / `file:read` / `media` / `media:read` (app keys too) |\n| `/v1/file/media/{id}/move` | PATCH | `file` / `file:write` / `media` / `media:write` |\n| `/v1/file/projects/{projectId}/fonts` | GET | `file` / `file:read` / `font` / `font:read` |\n| `/v1/file/projects/{projectId}/fonts/lookup` | GET | `file` / `file:read` / `font` / `font:read` |\n| `/v1/file/projects/{projectId}/fonts/{id}` | GET | `file` / `file:read` / `font` / `font:read` |\n| `/v1/file/projects/{projectId}/fonts/{id}` | PATCH | `file` / `file:write` / `font` / `font:write` |\n| `/v1/file/projects/{projectId}/fonts/{id}` | DELETE | `file` / `file:write` / `file:delete` / `font` / `font:write` / `font:delete` |\n| `/v1/file/projects/{projectId}/fonts/presigned-urls` | POST | `file` / `file:read` / `font` / `font:read` (app keys too) |\n| `/v1/file/fonts/{id}/move` | PATCH | `file` / `file:write` / `font` / `font:write` |\n| `/v1/file/projects/{projectId}/clips` | GET | `file` / `file:read` / `clip` / `clip:read` |\n| `/v1/file/projects/{projectId}/clips` | POST | `file` / `file:write` / `clip` / `clip:write` |\n| `/v1/file/projects/{projectId}/clips/{id}` | GET | `file` / `file:read` / `clip` / `clip:read` |\n| `/v1/file/projects/{projectId}/clips/{id}` | PATCH | `file` / `file:write` / `clip` / `clip:write` |\n| `/v1/file/projects/{projectId}/clips/{id}` | DELETE | `file` / `file:write` / `file:delete` / `clip` / `clip:write` / `clip:delete` |\n| `/v1/file/projects/{projectId}/clips/{id}/presigned-url` | POST | `file` / `file:read` / `clip` / `clip:read` to read |\n| `/v1/file/projects/{projectId}/clips/{id}/presigned-url` | POST | `file` / `file:write` / `clip` / `clip:write` to write (`?action=PutObject`) |\n| `/v1/file/projects/{projectId}/clips/{id}/move` | PATCH | `file` / `file:write` / `clip` / `clip:write` |\n| `/v1/file/projects/{projectId}/clips/{id}/duplicate` | POST | `file` / `file:write` / `clip` / `clip:write` |\n| `/v1/file/projects/{projectId}/clips/{id}/sync-size` | POST | `file` / `file:write` / `clip` / `clip:write` |\n\n`DELETE /v1/file/projects/{id}` moves a project to the trash (`?action=delete`, the default) and `?action=undelete` restores it; both return `204`, and the same `400` and `409` apply as for teams; restoring a project whose team is still in the trash is a `409` with `container_in_trash`. Its templates go to the trash with it and its media, fonts, clips and forms stay with it; none of them is listed in the trash on its own while the project is there, and restoring the project brings them all back. Once the retention window has passed, the scheduled deletion job removes the project for good with everything in it, its forms and the answers entered in them included.\n\nA media file is one image in up to four renditions, each `{ \"filename\", \"size\", \"storage_object\" }`: `file`, the print file the editor and the PDF use; `preview`, a web copy of at most 2560 × 2560 pixels the editor draws; `thumbnail`, at most 1024 × 1024, for lists; and `original`, the file as it was uploaded, in its own format (a TIFF or PSD from a template package too). `preview` is `null` until the image is processed. `original` is `null` for media stored before originals were kept, and for a package image that came only as an editor rendition. `width` and `height` give the image's size in pixels, of the file a download saves (the original, else the print file) and as it is shown, so a photo stored on its side has the two swapped; both are `null` for a vector (SVG, AI, EPS, PDF) and for a format that cannot be measured.\n\n`POST /projects/{projectId}/media/presigned-urls` reads despite the verb: it returns short-lived read links for up to 100 media files at once. Send `{ \"media_ids\": [\"...\"] }`, plus `include_thumbnail`, `include_preview` and `include_print` (each `true` by default) and `include_original` and `include_download` (both `false` by default) to choose the renditions. `download` is the original, or the print file for media without one, served as an attachment under the media's name (its extension added when the name has none), so a browser saves it rather than opening it; like the original, it needs read access to the project. You get one entry per media file found, with its `media_id` and a `thumbnail`, `preview`, `print`, `original` and `download` link each, `null` for a rendition you did not ask for or the file does not have. As with templates, ids from another project are left out rather than reported as an error.\n\n### Media tags\n\nEach project has its own tags for its media, and every media file carries its `tags`: `[{ \"id\", \"project_id\", \"name\", \"creator_id\", \"created_at\" }]`, in name order. A name is the tag: two names that differ only in case or spacing are the same one.\n\n`POST /v1/file/projects/{projectId}/media/tags` adds and removes tags on up to 100 media files at once, by name: send `{ \"media_ids\": [\"...\"], \"add_tags\": [\"Logos\"], \"remove_tags\": [\"Draft\"] }`. A name the project has no tag of yet becomes a new tag, so tagging is also how a tag is made. It answers the media files with their tags. A media file holds at most 20 tags and a project 500; beyond that it answers `409` with `tag_limit_reached`. An id that is no media file of the project is a `404`, and names that are empty or over 40 characters are a `422`. It needs edit rights on the project, as renaming does.\n\n`GET /v1/file/projects/{projectId}/tags` lists the project's tags in name order, each with `media_count`, the number of its media files out of the trash. `PATCH /v1/file/projects/{projectId}/tags/{id}` with `{ \"name\": \"...\" }` renames a tag on every media file; a name another tag of the project has already merges the two, and the answer is that other tag. `DELETE` on the same path removes the tag from every media file, those in the trash too, and answers `204`. Both need edit rights on the project.\n\nThe media list narrows to tags and names:\n\n| Query parameter | What it does |\n|---|---|\n| `tag_ids` | The ids of tags, comma separated: only media with these tags |\n| `tag_match` | `all` (the default): media with every tag in `tag_ids`; `any`: media with at least one |\n| `untagged` | `true`: only media without a tag; it takes no `tag_ids` |\n| `search` | Only media whose name contains this, in any case; at most 100 characters |\n| `sort` | `newest` (the default), `oldest`, `name` or `largest` |\n\n`total` counts the media that match. Moving a media file to another project takes its tags along by name, made in that project when it has no tag of the name yet; media in the trash keeps its tags.\n\n`POST /projects/{projectId}/fonts/presigned-urls` reads too: send `{ \"font_ids\": [\"...\"] }` (up to 100) and get one entry per font file found, `{ \"font_id\", \"presigned_url\": { \"url\", \"expires_at\", \"is_expired\", \"action\" } }`, with a read link valid for an hour, so a design can be drawn in its fonts. Ids from another project are left out.\n\nA clip is a small, project-scoped file resource (same shape as a font, minus the font-specific metadata): `id`, `project_id`, `team_id`, `creator_id`, `name`, `file`, `error`, `size`, `created_at`, `updated_at`, `touched_at`, `deleted_at`. `POST /projects/{projectId}/clips` creates one from `{ \"name\": \"...\" }`; `PATCH /projects/{projectId}/clips/{id}` renames it the same way. `GET /projects/{projectId}/clips` accepts `limit` (max 50, default 10) and `offset`, and returns `{ \"results\": [...], \"total\": <int> }`. `DELETE /projects/{projectId}/clips/{id}` soft-deletes (`?action=delete`, the default) or restores (`?action=undelete`) a clip, mirroring the template soft-delete pattern below.\n\nA clip's document JSON is stored in cloud storage, addressed via a presigned URL: `POST /projects/{projectId}/clips/{id}/presigned-url` returns a URL to read (`?action=GetObject`, the default — project view permission) or write (`?action=PutObject` — project edit permission) the clip body. The storage object is created lazily on the first write, so a read against a never-written clip returns `404`.\n\n`POST /projects/{projectId}/clips/{id}/sync-size` re-reads the clip's stored size from object metadata and returns the updated clip. A clip's document is uploaded straight to storage, so its size is not known at write time; the uploader calls this once the upload succeeds.\n\n`PATCH /projects/{projectId}/clips/{id}/move` re-parents a clip to another project (and, implicitly, that project's team), re-keying its stored document to the target folder — it requires edit permission on both projects. `POST /projects/{projectId}/clips/{id}/duplicate` instead creates a **copy** in the target project (view permission on the source, edit permission on the target): a new clip is returned (`201`) and the source's document JSON plus its thumbnail, when present, are copied to the new clip's own storage keys; the original stays put. Both take `{ \"project_id\": \"<target>\" }`.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/file/teams/019b.../projects \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"name\": \"Q4 Campaign\" }'\n```","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#templates","section":"Templates","audience":"api","headings":["Publishing a stored template","Touching after a save","Merge fields","Premade templates","Template comments"],"text":"Templates are the Typograph document files that the editor opens and the publisher renders. Templates are **project-scoped** on create/list — once you have a template ID you can read and update it directly.\n\n`GET /v1/file/projects/{projectId}/templates` lists the last edited template first, by `updated_at`, which moves when a template is created, saved, renamed or moved; opening one moves only `touched_at`. Templates edited at the same moment keep a fixed order; an edit while you page can still move a template to the front.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/projects/{projectId}/templates` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/templates` | POST | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/templates/presigned-urls` | POST | `file` / `file:read` (app keys too) |\n| `/v1/file/templates/{id}` | GET | `file` / `file:read` |\n| `/v1/file/templates/{id}/fields` | GET | `file` / `file:read` (app keys too) |\n| `/v1/file/templates/{id}` | PATCH | `file` / `file:write` |\n| `/v1/file/templates/{id}` | DELETE | `file` / `file:delete` |\n| `/v1/file/templates/{id}/touch` | PATCH | `file:write` |\n| `/v1/file/templates/{id}/move` | PATCH | `file` / `file:write` |\n| `/v1/file/templates/{id}/{fileType}/download-url` | GET | `file` / `file:read` |\n| `/v1/file/templates/{id}/{fileType}/presigned-url` | POST | `file:write` |\n| `/v1/file/templates/{id}/collaboration-token` | POST | `file:write` (people only) |\n\n`POST /projects/{projectId}/templates/presigned-urls` reads despite the verb: it returns short-lived read links for the thumbnails of up to 100 templates at once, so a grid can show a whole page of cards without a request per card. Send `{ \"template_ids\": [\"...\"] }` and you get one entry per template found, each with a `template_id` and a `thumbnail` that is `null` when the template has no thumbnail yet. Ids belonging to another project, or to a template in the trash, are left out of the response rather than reported as an error — the caller is authorized once for the project, not once per template.\n\n`POST /templates/{id}/collaboration-token` returns `{ \"token\", \"expires_at\", \"role\" }`: a two-minute token for the template's live room on the collaboration server, where everyone in the template sees each other's cursor and selection. The route needs a person's token with `file` / `file:write`, like saving; on the template itself, view permission is enough, and `role` says which the person has: `edit` for an owner or editor, `view` for a commenter or viewer. The editor asks for it through its host each time it connects, so a host never stores one. A caller who cannot view the template, or is an app rather than a person, gets `403`, and so does an unknown id, as on every template route; a server without live collaboration answers `503`. That `503` is not transient: fall back to single-user editing instead of retrying.\n\n`fileType` is one of `file` (the template's document) or `thumbnail`. `GET .../download-url` hands out a short-lived link to read the file and is the one an app key may call; `POST .../presigned-url` hands out write access and stays a person's job. The `touch` endpoint bumps the template's recency, and after a save its `updated_at` too (see [Touching after a save](https://docs.typograph.nl/guides/api/file-service#touching-after-a-save)). The `move` endpoints (`fonts`, `media`, `templates`) relocate a resource to another project: send `{ \"project_id\": \"...\" }`; the caller must be able to edit both the source and target project, and a missing, malformed, or inaccessible resource id uniformly returns `404`. `PATCH /projects/{id}/move` moves an entire project (including its fonts, media, and templates) to another team via `{ \"team_id\": \"...\" }`; a project id the caller cannot edit also reads as `404`. `DELETE /projects/{projectId}/media/{id}` and `DELETE /projects/{projectId}/fonts/{id}` work the same way: `?action=delete` (the default) moves the file to the trash, `?action=undelete` restores it, both return `204`, and the file's admins get a notification. Restoring a file that is not in the trash is a `400`; deleting one that is already there a `409` with `in_trash`, and restoring one whose project or team is still in the trash a `409` with `container_in_trash`. `DELETE /templates/{id}` soft-deletes a template (`?action=delete`) or restores it (`?action=undelete`); soft-deleted templates surface in the user's trash flow (see [Trash](https://docs.typograph.nl/guides/api/file-service#favorites-shared-trash-search-recents)) and are permanently removed — including their files in storage — by the scheduled deletion job once the retention window has passed.\n\n### Publishing a stored template\n\nTo make a PDF or an image of a template stored in Typograph, start a job on the template itself. The job is billed to the organization that owns the template, as when someone publishes it in the portal, so it needs a role that may spend that organization's quota.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/templates/{templateId}/jobs` | POST | `file` / `file:write` and `publisher` / `publisher:write` |\n| `/v1/file/templates/{templateId}/jobs/{jobId}` | GET | `file` / `file:read` |\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/file/templates/{templateId}/jobs \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"type\": \"publish\", \"format\": \"pdf\"}'\n```\n\n- `type` is `publish`. `format` is `pdf`, `jpg`, `jpeg`, `png` or `webp`. The merge options `csv_url`, `column_map`, `rows`, `merge_mode`, `filename_pattern` and `overflow` work as for a [publisher job](https://docs.typograph.nl/guides/api/publisher), and may each be `null` or left out.\n- It takes `edit` or `admin` on the template and `can_use_quota` in the organization that owns it, so a guest cannot start one. A token of an app bound to another organization than the template's is refused with `403` `organization_not_bound`: such an app publishes through the publisher's own routes, on its own organization's quota.\n- It counts against the owning organization's `publisher` limit; at the limit it answers `429` `rate_limit_exceeded`.\n\nIt answers `202`:\n\n```json\n{\n  \"job_id\": \"019b28fb-a11e-7641-a28f-e978f892ec20\",\n  \"template_id\": \"019b28fb-a11e-7641-a28f-e978f892ec21\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"type\": \"publish\",\n  \"status\": \"queued\",\n  \"created_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\n`GET .../jobs/{jobId}` answers for the person who started the job, with `view` on the template: the same fields plus `files`, `error` and `finished_at`. `status` is `queued`, `processing`, `completed` or `failed`. Each of `files` has a `filename`, a `download_url` that works for an hour, and its `expires_at`; `error` is `{code, message}` for a failed job and `null` otherwise.\n\n### Touching after a save\n\nThe document is written straight to storage through a presigned `PUT`, so `PATCH /templates/{id}/touch` is how the service learns of a save. The editor calls it on open, with no body, to drive recency lists. After writing the document, call it with `{ \"is_saved\": true }`: `updated_at` moves, integrations get a `template.updated` event, and the stored document is normalised (asset URLs cleared, image keys filled in), which may rewrite it. Both answer `204` with no body.\n\nTo keep to the version you wrote, send the `ETag` header your `PUT` answered as `etag`, quotes included:\n\n```bash\ncurl -X PATCH https://api.typograph.nl/v1/file/templates/019b.../touch \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"is_saved\": true, \"etag\": \"\\\"5d41402abc4b2a76b9719d911017c592\\\"\" }'\n```\n\nThe document is then read and rewritten only while it is still at that version, so a write someone made in between is never overwritten, and the response is `200` with the version the document has afterwards:\n\n```json\n{ \"etag\": \"\\\"7d793037a0760186574b0282f2f435e7\\\"\" }\n```\n\n`etag` is a new ETag when the document was normalised, and the one you sent when there was nothing to clean or the document could not be normalised this time (it is then still as you wrote it): send it as `If-Match` on your next `PUT`. It is `null` when the document changed after your write: it was left alone, and your next `PUT` with `If-Match` on your own version answers `412`, so you can load the newer document first. It is `null` as well when the document is no longer in storage, for instance because the template was deleted in the meantime. An `etag` that is not a non-empty string answers `400` with `invalid_request`.\n\n### Merge fields\n\n`GET /templates/{id}/fields` lists what a row of data can fill in a template, so you can prepare data for it, or let an agent fill rows, without reading the template's document. A template that has no saved document yet answers `400` with `invalid_request`, and one whose document cannot be read as JSON, or is larger than 20 MB, answers `400` with `invalid_template`.\n\nThe fields come from the template itself:\n\n| In the template | Field | `type` | Value |\n|---|---|---|---|\n| a text frame with a name | its name, e.g. `naam` | `text` | the frame's new text; a line break starts a paragraph |\n| an image with a name | its name, e.g. `photo` | `image` | an https URL of the picture, placed with the frame's own fit |\n| any element with a name, groups included | `<name>.visible`, e.g. `photo.visible` | `visible` | whether it prints: `true`/`false`, `yes`/`no`, `ja`/`nee`, `y`/`n`, `j`, `1`/`0` or `x`, in any case |\n| `{{voornaam}}` inside a text | the key, e.g. `voornaam` | `text` | the text in place of the placeholder |\n\n- **Only names someone gave count.** An element the editor named after its id, or one the InDesign import called `Element 12`, is no field: give the frames you want to fill a name in the editor's layers. Locked elements and master items are never fields.\n- **A placeholder must be filled.** Its field is `required`: your data must name it, or `{{voornaam}}` would print as it stands. An empty value removes the placeholder, and its line when nothing else is on it, which is what an address block with an empty second line needs.\n- **An empty value leaves an element as the template has it.** That holds for every other field.\n- **One name, one field.** Elements that share a name are filled together; `targets` lists each element by its `element_uuid` (the element's `uuid` in the template document), with its spread (counted from 1) and whether the field fills the element or a placeholder in it.\n\n```json\n{\n  \"template_id\": \"0199…\",\n  \"fields\": [\n    { \"key\": \"naam\", \"label\": \"naam\", \"type\": \"text\", \"required\": false, \"max_length\": null, \"choices\": null,\n      \"targets\": [{ \"element_uuid\": \"9b1e…\", \"spread\": 1, \"kind\": \"element\" }] },\n    { \"key\": \"voornaam\", \"label\": \"voornaam\", \"type\": \"text\", \"required\": true, \"max_length\": null, \"choices\": null,\n      \"targets\": [{ \"element_uuid\": \"41aa…\", \"spread\": 2, \"kind\": \"token\" }] }\n  ],\n  \"schema\": {\n    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n    \"type\": \"object\",\n    \"additionalProperties\": false,\n    \"required\": [\"voornaam\"],\n    \"properties\": {\n      \"naam\": { \"type\": \"string\", \"title\": \"naam\", \"description\": \"The text. An empty value leaves the template's text.\" },\n      \"voornaam\": { \"type\": \"string\", \"title\": \"voornaam\", \"description\": \"The text in place of {{voornaam}}. An empty value removes it, and its line when nothing else is on it.\" }\n    }\n  }\n}\n```\n\n`schema` describes one row as a JSON object, keyed by field: the shape an agent can fill and validate before it sends data. Its keys follow the JSON Schema specification (`additionalProperties`), not this API's snake_case. `max_length` and `choices` are `null` for now; they will come from the personalisation rules a template's editors set.\n\n### Premade templates\n\nThe premade template catalog itself is a **global, read-only** set of starter templates curated by Typograph. Unlike project templates, the catalog is identical for every user and is not project-scoped. Items are addressed by a URL-safe `code` (lowercase letters, digits, hyphens) rather than a UUID.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/premade-templates` | GET | `file` / `file:read` |\n| `/v1/file/premade-templates/categories` | GET | `file` / `file:read` |\n| `/v1/file/premade-templates/{code}` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/premade-templates/{code}/use` | POST | `file` / `file:write` |\n\nThe list endpoint accepts `limit` (max 100, default 50), `offset`, `category` (filter by category code), and `is_featured` (boolean) query parameters. The categories endpoint lists every active category, by name; with `has_templates=true` only the ones that hold at least one active template, as the Portal's gallery shows them. List responses are wrapped as `{ \"results\": [...], \"total\": <int> }`.\n\nThe `use` endpoint instantiates a catalog entry into a real, editable template: it downloads the entry's `.typograph` package, materialises its fonts and images as `Font`/`Media` records in `projectId`, and returns the new `Template` — the catalog entry itself is untouched. The package is imported exactly as a `.typograph` zip upload is: fonts already in the project are reused, and nothing is left in storage when the import fails. The caller must be able to edit `projectId`: a project the caller cannot edit (or that does not exist) returns `403`, a catalog `code` that does not exist returns `404`.\n\nOnly an entry with `is_usable: true` has a package. Using one that has none returns `400` with `error: premade_template_not_ready`; a package that cannot be downloaded returns `premade_template_unavailable`, and one that is not an importable zip `premade_template_invalid`.\n\n### Template comments\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/templates/{templateId}/comments` | GET | `file:read` |\n| `/v1/file/templates/{templateId}/comments` | POST | `file:write` |\n| `/v1/file/templates/{templateId}/comments/{commentId}` | PUT | `file:write` |\n| `/v1/file/templates/{templateId}/comments/{commentId}` | DELETE | `file:delete` |\n| `/v1/file/templates/{templateId}/comments/{commentId}/resolve` | PATCH | `file:write` |\n| `/v1/file/templates/{templateId}/comments/{commentId}/replies` | GET, POST | `file:read` / `file:write` |\n| `/v1/file/templates/{templateId}/comments/{commentId}/replies/{replyId}` | PUT | `file:write` |\n| `/v1/file/templates/{templateId}/comments/{commentId}/replies/{replyId}` | DELETE | `file:delete` |\n\nA comment is **anchored to a spot on a page**, not attached to the template as a whole. Its\n`position` carries `page_uuid` — the page the pin belongs to — plus `x` and `y` in native\nmillimetres. Millimetres rather than screen pixels, so a pin placed at one zoom level is in the\nsame place at another; the page uuid rather than a page number, so inserting or reordering pages\nahead of it does not move it. A comment whose `page_uuid` is an empty string predates anchoring\nand clients skip it rather than drawing it on page one.\n\nA comment's and a reply's `user` is its author as the pin shows them: `id`, `user_id`, `handle`,\n`first_name`, `last_name`, `full_name` and `image_url`. Everyone who may read the template sees it,\nso it never carries the author's email address, phone number or settings. `user_id` is the\nauthor's account id, the one to compare with the signed-in user; `id` is their record in the\nfile service.\n\n`GET /comments` returns a bare array, not an envelope. It accepts `limit`, `offset`, and\n`only_unresolved=true`, which is what drives the editor's default view — resolved threads are\nhidden behind a filter rather than deleted.\n\nWho may do what:\n\n| Action | Required |\n|--------|----------|\n| Read comments | `view` on the template |\n| Create a comment or reply | `comment` on the template |\n| Edit or delete a comment | Its **author** |\n| Edit or delete a reply | Its **author** |\n| Resolve or reopen a comment | Its author, **or** anyone with `edit` on the template |\n\nResolving is deliberately not author-only: the person who acts on the feedback is usually the\ndesigner, not the reviewer who wrote it, and a thread only they could close would stay open.\n\nReplying requires comment permission on the template, not authorship of the parent comment —\notherwise nobody could answer anyone else's feedback.\n\n`PUT` on a comment or a reply changes the body and nothing else. Moving a pin and resolving a\nthread are separate operations, and resolving carries a different permission.\n\nA comment's `is_resolved` says whether its thread is resolved. `PATCH .../comments/{commentId}/resolve?is_resolved=true`\nresolves it and `?is_resolved=false` opens it again; it takes no body and answers `204 No Content`.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#forms","section":"Forms","audience":"api","headings":[],"text":"A form turns a template into something an end user can personalise: each field writes a value into one element of the linked template. Forms are project-scoped, and a form is `draft` until it is published — a draft is only reachable by someone who can edit the project.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/templates/{templateId}/forms` | GET | `file` / `file:read` |\n| `/v1/file/templates/{templateId}/forms` | POST | `file` / `file:write` |\n| `/v1/file/projects/{projectId}/forms` | GET | `file` / `file:read` |\n| `/v1/file/projects/{projectId}/forms` | POST | `file` / `file:write` |\n| `/v1/file/forms/{id}` | GET | `file` / `file:read` |\n| `/v1/file/forms/{id}` | PATCH | `file` / `file:write` |\n| `/v1/file/forms/{id}` | DELETE | `file` / `file:delete` |\n| `/v1/file/forms/{id}/duplicate` | POST | `file` / `file:write` |\n| `/v1/file/forms/{id}/templates/{templateId}` | POST | `file` / `file:write` |\n| `/v1/file/forms/{id}/templates/{templateId}` | DELETE | `file` / `file:delete` |\n| `/v1/file/forms/{formId}/values/me` | GET | `file` / `file:read` |\n| `/v1/file/forms/{formId}/values/me` | POST | `file` / `file:write` |\n\nA form carries `id`, `project_id`, `team_id`, `creator_id`, `name`, `status` (`draft` or `published`), `fields`, `template_ids`, `created_at`, `updated_at` and `deleted_at`. Both list endpoints accept `limit` (max 50, default 10) and `offset`, and return `{ \"results\": [...], \"total\": <int> }`.\n\n`POST /templates/{templateId}/forms` creates a form and links it to that template in one step, inheriting the template's project and team; `POST /projects/{projectId}/forms` creates one in a project without a template. Both take only `{ \"name\": \"...\" }`, answer `201`, and start the form as a draft without fields: fields and steps are set with `PATCH /forms/{id}`, as are further template links. `PATCH /forms/{id}` is a partial update — only the properties present in the body change, and `template_ids` replaces the whole link set. `POST /forms/{id}/duplicate` copies the form, its fields and its template links as a draft; the template itself is **not** duplicated, so the copy points at the same template and the fields' `element_uuid` references stay valid.\n\nEach field carries an `id`, a `type` (`text`, `textarea`, `number`, `color`, `date`, `dropdown`, `radio`, `checkbox`, `toggle` or `image`), and optionally `label`, `description`, `required`, `default_value`, `element_uuid`, `canvas_action` and `image_source`. `element_uuid` is the `uuid` of the template element the value is written to, and `canvas_action` says how: `TEXT` into a text element's runs, `URL` into an image, `COLOR` into a fill (or a line's stroke), `HIDDEN` to leave the element alone. It defaults to `URL` for an image field, `COLOR` for a colour field, and `TEXT` otherwise — a mismatch between the action and the element's kind means the value is silently not applied, so the portal's form builder blocks those combinations up front. An element locked in the editor, or nested inside a clip, cannot be personalised at all.\n\n`DELETE /forms/{id}` deletes a form at once: it is no longer read or listed. It cannot be restored, and the scheduled deletion job removes it for good once the retention window has passed, with the answers entered in it.\n\n`GET|POST /forms/{formId}/values/me` reads and writes the **calling user's own** answers, as `{ \"values\": { \"<key>\": <value> } }`. A field's key is `<element_uuid>::<canvas_action>` when it is bound to an element (`9b1e…::URL`), and its `id` when it is not. A user who has not filled the form in yet gets an empty `values` object rather than a `404`. Reading takes a read scope and writing a write scope, so a read-only integration can fetch answers without being able to change them.\n\nA form whose template is in the trash is not in the trash itself, but it fills in nothing until the template is restored. Its `template_ids` leave that template out, a duplicate does not link it, and filling the form in, or an app key reading it, answers `409` with `in_trash`, naming the template (see [Nothing works on what is in the trash](https://docs.typograph.nl/guides/api/file-service#nothing-works-on-what-is-in-the-trash)). Whoever builds the form still reads and changes it, to link another template or delete it. Restoring the template brings the link back.\n\nThe write accepts `is_complete` beside `values`. A draft is stored as it comes, so a form being typed into can be saved at any moment; `is_complete: true` marks the finished answer, and a required field left empty is then rejected with a `400`. Only the keys of the form's own fields are stored: any other key is dropped, and the response shows what was kept.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#sharing-invites-roles-permissions","section":"Sharing: Invites, Roles, Permissions","audience":"api","headings":["Editor seats","Asking for access"],"text":"Sharing is **resource-scoped**: you invite someone to a specific team, project, or template. The resource type is part of the URL.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/{resourceType}/{resourceId}/permissions` | GET | `file:read` |\n| `/v1/file/{resourceType}/{resourceId}/invitations` | GET, POST | `file:read` / `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/invitations/{id}` | GET | `file:read` |\n| `/v1/file/{resourceType}/{resourceId}/invitations/{id}` | PATCH | `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/invitations/{id}` | DELETE | `file:delete` |\n| `/v1/file/{resourceType}/{resourceId}/roles/{id}` | PATCH | `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/roles/{id}` | DELETE | `file:delete` |\n| `/v1/file/{resourceType}/{resourceId}/access-requests` | GET, POST | `file:read` / `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/access-requests/{id}/approve` | POST | `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/access-requests/{id}/decline` | POST | `file:write` |\n| `/v1/file/{resourceType}/{resourceId}/access-requests/{id}` | DELETE | `file:delete` |\n\n`resourceType` is one of `team`, `project`, or `template`. Asking for access is described under [Asking for access](https://docs.typograph.nl/guides/api/file-service#asking-for-access).\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/file/template/019b.../invitations \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"email\": \"colleague@example.com\", \"permission\": \"edit\" }'\n```\n\nAn editor invites with `view`, `comment` or `edit`; only an admin of the resource can invite\nsomeone as `admin`, or change a pending invitation to it with\n`PATCH /{resourceType}/{resourceId}/invitations/{id}` and `{ \"permission\": \"admin\" }`. Anything\nelse is refused with a `403` and `error: access_denied`. Accepting checks this again against\nwhoever sent the invitation, as they are at that moment: an invitation its sender can no longer\ngrant (a demoted admin's `admin` invitation) is refused with a `403` and stays pending until it is\nchanged, withdrawn or sent again.\n\nThe invited user answers an invitation with `PATCH /{resourceType}/{resourceId}/invitations/{id}?action=accept`\nor `?action=ignore`; both return `204 No Content`. Only the user whose email address the invitation was\nsent to may answer it. Anyone else gets a `403` with `error: invite_recipient_mismatch` and the\ninvitation's address masked in `recipient_hint`, enough to tell which account to sign in with:\n\n```json\n{\n  \"error\": \"invite_recipient_mismatch\",\n  \"error_description\": \"This invitation was sent to j•••@example.com. Sign in with that address to respond to it.\",\n  \"recipient_hint\": \"j•••@example.com\"\n}\n```\n\nAn invitation whose team, project or template has been deleted, or is in the trash or in a project or team in the trash, answers `404` to the invitee who accepts it; ignoring it still works (`204`), so the invitee can clear it. Whoever manages the invitations of something in the trash gets `409` with `in_trash` instead (see [Nothing works on what is in the trash](https://docs.typograph.nl/guides/api/file-service#nothing-works-on-what-is-in-the-trash)).\n\n`GET /{resourceType}/{resourceId}/permissions` returns what the calling user may do with the\nresource: `can_view`, `can_comment`, `can_edit` and `can_administer`. The flags cascade — admin\nimplies edit, edit implies comment, comment implies view — so a client can test the one flag it\ncares about without recombining them. Beside them it says where that access comes from:\n\n```json\n{\n  \"can_view\": true,\n  \"can_comment\": true,\n  \"can_edit\": false,\n  \"can_administer\": false,\n  \"permission\": \"comment\",\n  \"granted_on\": {\n    \"resource_type\": \"project\",\n    \"resource_id\": \"0193a4b2-7c1e-7d40-9f2a-3b5c6d7e8f90\",\n    \"name\": \"Spring campaign\"\n  },\n  \"inherited_permission\": \"edit\",\n  \"expires_at\": null,\n  \"admins\": [{ \"name\": \"Anna de Vries\" }]\n}\n```\n\n- `permission` is the role that counts here (`view`, `comment`, `edit` or `admin`), and `granted_on`\n  the team, project or template it is given on.\n- `inherited_permission` is the role from the place above, set only when it differs: here an editor\n  of the team was given `comment` on this project on purpose.\n- `expires_at` is when the role ends, or `null`.\n- `admins` names whom to ask for more: the admins of the nearest place that has any, at most 10. Only\n  the places the caller holds a role on, and those below them, count. So someone who shares one\n  project is not told who the admins of its team are.\n\nWithout access, `permission` and `granted_on` are `null` and `admins` is empty.\n\nThe role closest to the resource counts, also when it is lower: a role on a template wins over one\non its project, and one on a project over one on its team, so an editor of a team can be given\n`comment` on one project. An `admin` role is the exception: an admin of a team stays admin of every\nproject and template in it, and an admin of a project of every template in it, whatever role they\nhold there. Giving such an admin less on a project or template is refused with `409` and\n`error: admin_inherited`, by an invitation (`POST .../invitations`), a change of one\n(`PATCH .../invitations/{id}`), accepting one (`?action=accept`, which leaves it pending) and a\nchange of a role (`PATCH .../roles/{id}`). `admin_resource_type` says whether they are admin of the\n`team` or the `project`; neither is named, as a caller who shares one project may not see its team:\n\n```json\n{\n  \"error\": \"admin_inherited\",\n  \"error_description\": \"ada@example.com is admin of the team this project is in, and stays admin here. Make them admin here, or leave their access as it is.\",\n  \"admin_resource_type\": \"team\"\n}\n```\n\nTo the invitee accepting, `error_description` reads \"You are admin of the team this project is in,\nand stay admin here. This invitation would give you less: decline it.\" Inviting or changing them to\n`admin` is always allowed.\n\nThe four permission levels, in increasing order of access:\n\n| Permission | Can |\n|------------|-----|\n| `view` | Open and download the resource. |\n| `comment` | The above, plus leave and reply to comments, and resolve comment threads. Cannot change the design. |\n| `edit` | The above, plus change the resource and invite others. |\n| `admin` | Everything, including managing access and deleting. |\n\nThe authoritative list of roles and their capabilities lives on the Identity Service — see [Identity Service](https://docs.typograph.nl/guides/api/identity-service).\n\n### Editor seats\n\nEvery plan has a number of **editor seats**: `editor_seats` in the plan's `features` ([`GET /v1/subscription/me`](https://docs.typograph.nl/api-reference)), with the seats bought as add-ons included. Free has one, the owner's, so everyone it shares with can view or comment; Pro has 5 and Enterprise 10. An editor is a person who holds `edit` or `admin` anywhere in the organization, on a team or on anything in one, an open invitation included; a role on something in the trash counts until it is deleted for good. One person takes one seat, however many things they can edit. `view` and `comment` never take a seat and are never limited.\n\nGranting `edit` or `admin` to someone who holds no seat yet is refused once the seats are all taken, with `409` and `editor_seats_taken`. That holds for an invitation (`POST .../invitations`), a change of one (`PATCH .../invitations/{id}`), accepting one (`?action=accept`), a change of a role (`PATCH .../roles/{id}`), approving a request for access (`POST .../access-requests/{id}/approve`) and creating a team (`POST /v1/file/teams`), which makes its creator the team's admin. Being the organization's owner or an admin takes no seat by itself: only creating a team or being granted `edit` or `admin` does. The message says what to do:\n\n| When | `error_description` |\n|------|---------------------|\n| Inviting or changing, on a plan with seats | Your plan has 5 editor seats, all taken. Invite to view or comment, free a seat, or request another plan. |\n| Inviting or changing, on Free | Free has 1 editor seat, the owner's, so shares can view or comment. Invite to view or comment, or request another plan. |\n| Creating a team | Creating a team makes you its admin, which takes an editor seat, and your plan's 5 editor seats are all taken. Free a seat, or request another plan. |\n| Accepting | This invitation can't be accepted yet: the organization's editor seats are all taken. Ask whoever invited you to free a seat, or to invite you to view or comment. |\n\nAn invitation holds its seat only if there was room for it, also one sent before the plan had a limit: accepting it is refused while the seats are taken, and it stays pending. Editors who exist already keep their access, also beyond the plan. A seat comes free when its person no longer holds `edit` or `admin` anywhere, for example after a role is set to `view` or removed, or an invitation withdrawn. When the plan cannot be checked, these calls answer `503` with `subscription_service_unavailable` and change nothing. `GET /v1/subscription/usage` shows the seats in use against the limit, in `plan_usage.editor_seats`.\n\nWho holds a seat already is said per person: `is_editor` on the roles of `GET /v1/file/roles`, for a caller who may manage access to the resource, and on the invitations of `GET /v1/file/{resourceType}/{resourceId}/invitations`. When it is `true`, `edit` or `admin` takes that person no new seat, so it is not refused when the seats are all taken. It is `null` where it is not said.\n\n### Asking for access\n\nSomeone who can open a team, project or template can ask its admins for a higher role there: `comment`, `edit` or `admin`. The admins are notified, and one of them, or an editor for a role up to `edit`, approves or declines it. Like the rest of sharing, these endpoints take a user token only.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/file/project/0193a4b2-7c1e-7d40-9f2a-3b5c6d7e8f90/access-requests \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"permission\": \"edit\", \"message\": \"The date of the campaign moved.\" }'\n```\n\n`permission` is required; `message` is optional, at most 500 characters, and trimmed. The answer is `201 Created` with the request:\n\n```json\n{\n  \"id\": \"0193a4b2-8d2f-7e51-a03b-4c6d7e8f9a01\",\n  \"requester\": {\n    \"id\": \"0193a4b2-6b0d-7c3f-8e19-2a4b5c6d7e8f\",\n    \"user_id\": \"0193a4b2-5a9c-7b2e-9d08-1f3a4b5c6d7e\",\n    \"image_url\": null,\n    \"handle\": \"vera\",\n    \"first_name\": \"Vera\",\n    \"last_name\": \"Viewer\",\n    \"full_name\": \"Vera Viewer\"\n  },\n  \"resource_type\": \"project\",\n  \"resource_id\": \"0193a4b2-7c1e-7d40-9f2a-3b5c6d7e8f90\",\n  \"resource_name\": \"Spring campaign\",\n  \"permission\": \"edit\",\n  \"message\": \"The date of the campaign moved.\",\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"expires_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\n`requester` is the public profile, as a comment's author: whoever answers sees no email address. `resource_name` is the name the place had when it was asked for. `expires_at` is 30 days after `created_at`.\n\nAsking is refused:\n\n- with `403` and `access_denied` by someone who cannot open the place: only who sees it may ask for more on it;\n- with `400` and `invalid_request` for a role no higher than the caller holds there already;\n- with `400` and `validation_error` for a `permission` other than `comment`, `edit` or `admin` (`view` is what every requester has), or a `message` over 500 characters;\n- with `409` and `access_request_open` when the caller asked for this place already and that request is still open: withdraw it to ask for another role;\n- with `409` and `access_request_limit_reached` when the caller has 20 open requests.\n\nThe admins of the place are told with an `access_request` notification; when it has none, the admins of the nearest project or team above that has any, at most 10. Withdrawing and asking again for the same place tells them only once a day.\n\n**Who answers.** `GET /{resourceType}/{resourceId}/access-requests` lists the open requests on the place that the caller may answer, oldest first, as a bare array of the requests above (`limit`, default `10`, max `50`, and `offset`). It needs `edit` on the place, or answers `403` with `access_denied`. An editor answers requests for up to `edit` from people who have no role on the place itself; a request for `admin`, and a request to raise a role someone holds on the place already (also one set lower there on purpose), takes an admin. A request the caller may not answer is left out of the list, and answering it is a `403`.\n\n`POST .../access-requests/{id}/approve` gives the requester the role on the place itself, where it counts as their closest role, and answers `200` with that role. Without a body it gives the role asked for; `{ \"permission\": \"comment\" }` gives another one instead, lower or higher, within what the caller may give. A role the requester holds already on the place is raised; an end date it has stays. Approving is refused:\n\n- with `400` and `invalid_request` for a role no higher than the requester holds by now;\n- with `403` and `access_denied` when the caller may not give the role;\n- with `409` and `editor_seats_taken` for `edit` or `admin` to someone who holds no editor seat yet, while the plan's seats are all taken ([Editor seats](https://docs.typograph.nl/guides/api/file-service#editor-seats)); `503` with `subscription_service_unavailable` when the plan cannot be checked.\n\n`POST .../access-requests/{id}/decline` declines it, without a body, and answers `204 No Content`. It takes what approving the request would take.\n\nOnce a request is approved, declined or withdrawn it is gone: answering it again is a `404` with `not_found`, as is a request id of another place. Two answers at the same moment get one through; the other is refused with `400` and `invalid_request`. A role given another way, such as an invitation, answers the request too: it is left out of every list from then on, and the requester can ask again.\n\n**The requester.** `GET /v1/file/users/me/access-requests` lists the caller's own open requests, newest first (`limit`, default `10`, max `50`, and `offset`), leaving out one whose place is in the trash or deleted. `DELETE /{resourceType}/{resourceId}/access-requests/{id}` withdraws one and answers `204 No Content`; only the requester may, anyone else gets `404` with `not_found`.\n\nThe requester is told the answer with an `access_granted` or `access_declined` notification. Its `sender` is who answered, or `null` when that is an admin of a team or project above that the requester does not share. A request that gets no answer lapses at `expires_at`: from then on it is in no list and cannot be answered, and within a day it is removed and the requester gets an `access_request_expired` notification, from Typograph itself.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#favorites-shared-trash-search-recents","section":"Favorites, Shared, Trash, Search, Recents","audience":"api","headings":["Nothing works on what is in the trash"],"text":"| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/favorites` | GET | `file` / `file:read` |\n| `/v1/file/favorites` | PUT | `file` / `file:write` |\n| `/v1/file/shared` | GET | `file` / `file:read` |\n| `/v1/file/shared/projects` | GET | `file` / `file:read` |\n| `/v1/file/shared/templates` | GET | `file` / `file:read` |\n| `/v1/file/trash` | GET | `file` / `file:read` |\n| `/v1/file/search` | GET | `file` / `file:read` |\n| `/v1/file/recent/searches` | GET | `file` / `file:read` |\n| `/v1/file/recent/searches` | POST | `file` / `file:write` |\n| `/v1/file/recent/searches` | DELETE | `file` / `file:write` |\n| `/v1/file/recent/templates` | GET | `file` / `file:read` |\n\n`PUT /v1/file/favorites` toggles the favorite state of a project or template: send `{ \"resource_type\": \"project\", \"resource_id\": \"...\" }` (or `\"template\"`). `GET /v1/file/favorites` lists both, as `{ \"projects\": [...], \"templates\": [...] }`, each favorite with the whole project or template; `limit` (default `10`, max `50`) and `offset` apply to each kind separately. A favorite of what is in the trash, or in a trashed project or team, is left out until it is restored, and none can be added: that is a `409` with `in_trash`.\n\n`GET /v1/file/shared?limit=&offset=` is one feed of what others shared with the user: projects and templates shared with the authenticated user (roles shared with one of the user's teams, and expired roles, are excluded), newest-first. `limit` defaults to `10` and caps at `50`; exceeding it returns `400`. Pass `resource_type=project` or `resource_type=template` to narrow the feed to one kind (any other value is a `400`; teams never appear here). The response is `{ \"results\": [...], \"total\": n }`. Each row in `results` carries `resource_type` (`project` or `template`), the resource's `id`/`name`/`team_id`, `project_id` and `thumbnail_filename` (template-only, otherwise `null`; it holds the thumbnail's filename in storage, not a link: read the thumbnail through the template presigned URLs), `template_count` (project-only, otherwise `0`), `shared_by_name`/`shared_by_email` of the person who shared it (`null` if that user has since been deleted), `shared_at`, `permission` (`view` / `comment` / `edit` / `admin`), and the shared resource itself as a nested `project` or `template` object (whichever matches `resource_type`; the other is `null`) so clients can render it with the same card they use elsewhere. `total` counts every matching role; rows whose resource has since been deleted are silently skipped, so a page can legitimately contain fewer than `limit` items.\n\n`GET /v1/file/shared/projects` and `GET /v1/file/shared/templates` list one kind each: the projects the user was invited to and holds a role on directly (not through a team), or the templates they hold a role on directly (not through a team or a project), the last edited first (by `updated_at`, as the lists of a team and a project), as `{ \"results\": [...], \"total\": n }` with the same project and template objects as elsewhere. They take `limit` (default `10`, max `50`) and `offset`. Where `/v1/file/shared` is the feed of what is shared with the user, these differ in two ways: they put the last edited first where the feed puts the newest share first, and they return the plain project or template objects, without who shared it, when, or the permission. Like that feed, they count a role that has expired as none, and leave out an item in the trash or in a project or team in the trash.\n\n`GET /v1/file/trash?limit=&offset=` lists what is in the user's trash: `{ \"limit\": n, \"offset\": n, \"teams\": [...], \"projects\": [...], \"templates\": [...], \"clips\": [...], \"media\": [...], \"fonts\": [...] }`. Each array holds the trashed items of that kind which the user holds a role on, directly or through their team, in the same shape as everywhere else plus `deleted_at` and `can_restore`. `can_restore` says whether this user may restore the item, by the same rule as its restore endpoint: a team takes a team admin, a project or template its admin (the most specific role counts, so a team admin unless the item has a role of its own), and a clip, media file or font anyone who can edit its project. The endpoint stays the gate, so check the flag before offering a restore, not instead of handling a `403`. A template or media file also carries `thumbnail_url`, a read link to its thumbnail that is valid for 60 minutes, or `null` when it has none: the one way to show what it looks like while it is in the trash, as a template's own thumbnail endpoints answer `409` with `in_trash` and the media read links leave a trashed file out. A file inside a trashed project is not listed on its own: restoring the project brings it back. `limit` (default `10`, max `50`) and `offset` apply to each kind separately. Restore an item with the `undelete` action of its own `DELETE` endpoint.\n\n`GET /v1/file/search` matches the `query`, 2 to 255 characters without the spaces around it (outside that is a `400` with `invalid_request`), against the names of the templates, projects, teams, media and fonts the user can open, returning each group (`templates`, `projects`, `teams`, `media`, `fonts`), the most recently updated first, plus a `total`, the `query`, `limit` and `offset` it ran with, and `breadcrumbs`: the project and team name of each template, media file, font and project, by its id. A media file or font is found where the user may open its project's media and fonts: a role on the project or its team, or on a template in the project. Five parameters narrow the search, and only narrow it: `types[]` lists only those types (`templates`, `projects`, `teams`, `media`, `fonts`; every type when left out); `team_ids[]` and `project_ids[]` keep what is in one of those teams or projects, in either when both are given, at most 100 of each (a team itself is listed only when named in `team_ids[]`); `min_updated_at` keeps what was updated at or after an ISO 8601 date-time or date; `max_updated_at`, in the same forms, keeps what was updated before it, not at it, so last month is `min_updated_at` the first of last month and `max_updated_at` the first of this month (an end at or before the start is a `400` with `invalid_request`). With one of these five the `query` may be left out: the search then lists everything the filters hold, whatever its name, and remembers no recent search (`?types[]=templates&min_updated_at=…` lists the templates changed since then, by anyone, and with `&max_updated_at=…` those changed in that period). Without a query and without a filter it is a `400`. A team or project the user cannot see finds nothing, and an unknown type, an id that is no UUID or a date that cannot be read is a `400` with `invalid_request`. Beside the lists, `template_count`, `project_count`, `team_count`, `media_count` and `font_count` say how many of each type the search finds in its place and date, whatever `types[]` lists and however many fit in `limit`: the numbers behind a type filter. Pass `?save=0` to run the search without recording it as a recent search (used for search-as-you-type); the default `save=1` records the query. Pass `?include_deleted=1` to include soft-deleted items in the results: the response will then also contain `trashed_teams`, `trashed_projects`, and `trashed_templates` arrays (same shape as their non-trashed counterparts, the most recently deleted first); omitting the flag (default `0`) excludes trashed items. `GET /v1/file/recent/searches` lists the current user's recent queries, the most recent first, as a bare array of `{ \"id\", \"user_id\", \"query\", \"touched_at\" }` (`limit`, default `10`, max `50`, and `offset`). `POST /v1/file/recent/searches` saves a query (`{ \"query\": \"...\" }`, trimmed, 2 to 255 characters) and `DELETE /v1/file/recent/searches` clears the current user's recent searches; both return `204 No Content`.\n\nThese lists, and the teams, shared items, favorites, recently opened templates and notifications, hold only the organization the token reaches, also when the user has roles in others ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)).\n\nIn `GET /v1/file/teams`, the shared lists, the trash, search, favorites and recently opened templates, every team, project, template, media file, font and clip carries `organization_id` and `organization_name` at its top level: the organization that owns it. For your app's token that is always the organization the token reaches. The fields are there for the portal's assistant, which works for a person in every organization they belong to and lists them side by side.\n\n### Nothing works on what is in the trash\n\nSomething is in the trash when it, its project or its team was deleted (`deleted_at` is set). Until it is restored, no call works on it: reading it, its file or its thumbnail, changing, sharing, commenting, favoriting, moving or copying it, and creating, uploading or moving anything into a project or team in the trash. Each answers `409 Conflict` with `in_trash`, and names what is in the trash: the item itself, or the outermost project or team that is.\n\n```json\n{\n  \"error\": \"in_trash\",\n  \"error_description\": \"This template is in a project that is in the trash. Restore the project first.\",\n  \"resource_type\": \"project\",\n  \"resource_id\": \"019397ab-cdef-7890-abcd-ef1234567890\",\n  \"deleted_at\": \"<ISO 8601 timestamp>\",\n  \"request_id\": \"...\"\n}\n```\n\n`resource_type` is `team`, `project`, `template`, `clip`, `media` or `font`. Restore that one with the `undelete` action of its own `DELETE` endpoint. Only a caller who may reach the item learns that it is in the trash; anyone else gets the `403` or `404` they always got.\n\nWhat still works: `GET /v1/file/trash`, and the restore. A restore of something whose project or team is still in the trash is refused with `409` and `container_in_trash`, naming that project or team, as its scheduled deletion would take the item with it: restore the project or team first.\n\nLists are not refused: they leave out what is in the trash, and what is in a project or team in the trash. `GET /trash` is where it is listed, and `/search` finds it with `include_deleted=1`, in `trashed_*`. The lists no longer read `include_deleted`: a request that still sends it gets what is out of the trash. `POST /projects/{projectId}/templates/presigned-urls` leaves out a template in the trash rather than refusing. An invitee who accepts an invitation into something in the trash gets `404`, as for a deleted one.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#users-roles--invitations-current-user","section":"Users, Roles & Invitations (Current User)","audience":"api","headings":[],"text":"| Endpoint | Method | Scopes | Notes |\n|----------|--------|--------|-------|\n| `/v1/file/users` | PATCH | `file` / `file:write` | Updates the authenticated user's profile in the file service. |\n| `/v1/file/users/{id}` | GET | `file` / `file:read` | Fetch a user's profile by UUID (`me` for the caller). Only the user themself and people who share an organisation with them get it, and with a token bound to an organisation only the people in that one; anyone else gets a `404`. |\n| `/v1/file/users/me/invitations` | GET | `file` / `file:read` | List invitations addressed to the current user, to teams, projects and templates of the token's organization. |\n| `/v1/file/users/me/access-requests` | GET | `file` / `file:read` | List the current user's own open requests for access, newest first ([Asking for access](https://docs.typograph.nl/guides/api/file-service#asking-for-access)). |\n| `/v1/file/roles` | GET | `file` / `file:read` | Who has access to a team, project or template: the roles people hold on it (`resource_types[]` and `resource_ids[]` in pairs), with `is_editor` for a caller who manages its access. |","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#notifications","section":"Notifications","audience":"api","headings":[],"text":"| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/file/notifications` | GET | `file` / `file:read` |\n| `/v1/file/notifications` | PATCH | `file` / `file:write` |\n| `/v1/file/notifications/{id}` | PATCH | `file` / `file:write` |\n\nA notification's `sender` is the person who caused it, with the same public fields as a comment's author: `id`, `user_id`, `handle`, `first_name`, `last_name`, `full_name` and `image_url`, never their email address, phone number or settings. It is `null` for a message from Typograph itself.\n\nA notification's `message` is one English sentence, an HTML fragment with the names in bold. To show the notification in another language, word it yourself from its `type` and `parameters`: the values the message is built from, as plain text. Which keys a type carries:\n\n| Type | `parameters` |\n|------|----------|\n| `delete_*`, `undelete_*`, `purged_*` | `name` |\n| `delete_warning_*` | `name`, `days` (an integer) |\n| `invite_*` | `name`, `permission` (`view`, `comment`, `edit` or `admin`) |\n| `new_comment` | `name` (the template's), `template_id`, `comment_id`, `excerpt`, `reply` (`true` for a reply in a thread) |\n| `access_request` | `name`, `permission` (the role asked for), `resource_type`, `resource_id`, `access_request_id` |\n| `access_granted` | `name`, `permission` (the role given), `resource_type`, `resource_id` |\n| `access_declined`, `access_request_expired` | `name`, `permission` (the role asked for), `resource_type`, `resource_id` |\n\nWho caused it is `sender`, not a parameter. `parameters` is `null` on a notification it could not be read back for, and on a `mention_comment`: show the `message` then.\n\nMark a single notification read with `PATCH /notifications/{id}?mark=read` (the default), or unread again with `?mark=unread`. Mark **all** notifications read in one call with `PATCH /notifications`, optionally only those of one team with `?team_id=`. Neither takes a body; both answer `204 No Content`.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#uploads","section":"Uploads","audience":"api","headings":["Template uploads","Canva PDF uploads"],"text":"Large uploads use **presigned URLs** — you request one from the File Service, then `PUT` the file bytes directly to cloud storage, bypassing the API. A media file, font or template package is uploaded this way, through an upload; template thumbnails and documents have their own `presigned-url` endpoints (listed in the relevant sections above). The finalize call refuses a file over its limit with `413` and error `payload_too_large`: 75 MB for an image, 25 MB for an SVG, 50 MB for a font (template packages and PDFs below).\n\nA thumbnail upload URL is signed for one `Content-Type`: `image/webp` for a template thumbnail, `image/jpeg` for a clip thumbnail. Send exactly that header with the `PUT`; storage refuses any other type with `403`. A thumbnail read URL always serves the image with that type.\n\nFlow:\n\n```\nPOST  /v1/file/projects/{projectId}/uploads        # create the upload: upload_job, and presigned_url to PUT to\nPUT   <signed_url>                                 # upload bytes to R2/S3\nPATCH /v1/file/projects/{projectId}/uploads/{id}   # finalize metadata\nGET   /v1/file/projects/{projectId}/uploads/{id}   # follow it until it ends\n```\n\nAn upload's `status` is `awaiting_upload` until the finalize call and `received_upload` after it,\n`processing_upload` while the file is imported, and `importing_assets` while a template's fonts and\nimages are still being imported in the background (the template exists already). It ends `success`\nor `failed`, with the reason in `message`. `converting` is for a PDF converted in the background,\nbefore its import; today a `.pdf` is still converted inside the finalize call (below), so no upload\nhas it yet. Read `status` by value, and treat one you do not know as still running. `GET /v1/file/projects/{projectId}/uploads/{id}` reads one upload,\nfor anyone who can view the project (scope `file` or `file:read`).\n\n### Template uploads\n\nUploading a Typograph template export (`.zip` with embedded fonts/images, or a bare `.json` body) follows the same flow, but the finalize call processes the file **synchronously**: fonts and images are extracted, deduplicated against the project, and a new template is created. On success the upload's `message` field contains the new template id and `warnings` lists any assets the template references that could not be resolved. Size limits: 5 MB for `.json`, 50 MB for `.zip` (`413` with error `payload_too_large` when exceeded), and 250 MB for what a `.zip` holds once unpacked. Structurally invalid templates are rejected with `422` and error `validation_error`, the schema violations in `error_description`.\n\nA package can mark a font of its manifest `\"source\": \"pdf\"`: a copy a PDF embedded, cut down to the letters the design uses, with those letters in `available_characters`. Such a font is imported as a **preview** of its template, never as a font in its own right: it is stored with `\"source\": \"pdf\"` and its `available_characters`, the template shows the text in it, and it is still listed in `warnings` as an `asset_missing` font, with the preview's id as `source_id`. Upload the complete font to the project and it takes the preview's place in the template. When the project already has a font of that name, the template uses that one and the preview is not imported. A preview is never used for another template's font of the same name.\n\n### Canva PDF uploads\n\nA PDF exported from Canva (`.pdf`, up to 50 MB) can be uploaded the same way and becomes a template. The finalize call converts the PDF to a template package first and then imports that package exactly like a `.zip` upload, so the response is the same: `message` holds the new template id, and the upload's `filename` comes back ending in `.zip`.\n\nEach PDF page becomes one page of the template. Text, photos, vector shapes and solid colours are carried over as editable elements.\n\nA PDF only holds its fonts cut down to the letters the design uses, so the font files are not carried over. Each font the text uses is looked up by family and style among the project's fonts. A font the project does not have and that [Google Fonts](https://fonts.google.com) has is added to the project in full. A font that is still missing is shown in the default font until you upload it; once it is uploaded, the template uses it.\n\n`warnings` says what the import did and what it could not do, one entry per item, each with a `code`:\n\n| `code` | Other fields | Meaning |\n|---|---|---|\n| `pdf_image_unsupported` | `page`, `message` | An image in a format, compression or colour space that is not read (CMYK pixels that are not a JPEG, 16-bit); the reason is in `message`. |\n| `pdf_image_damaged` | `page`, `message` | An image whose pixel data is broken. |\n| `pdf_image_uncropped` | `page`, `message` | An image cropped by more than one outline at once, placed uncropped. |\n| `pdf_image_transparency_lost` | `page`, `message` | A partly transparent image whose transparency could not be kept (a CMYK JPEG, or a mask that cannot be read), placed without it. |\n| `pdf_gradient` | `page`, `message` | Shapes filled with a gradient or a pattern, not carried over. |\n| `pdf_spot_color` | `page`, `message` | Shapes in a spot colour, not carried over. |\n| `pdf_shape_unsupported` | `page`, `message` | Shapes not carried over for another reason; the reason is in `message`. |\n| `pdf_shape_uncropped` | `page`, `message` | Shapes cropped in a way that could not be kept, shown whole. |\n| `pdf_font_default` | `page`, `message` | Text in a font the PDF gives no name (emoji, some text effects), shown in the default font. |\n| `font_added` | `source` (`google`), `name` | A font added to the project from Google Fonts, as a person reads it (`\"Montserrat Bold\"`). |\n| `asset_missing` | `kind` (`font` or `image`), `name`, `source_id` | A font or image the template uses that the project does not have, as for a `.zip` upload. |\n\nThe `pdf_` warnings say what could not be carried over: `page` is the page it is on (counted from 1), and `message` says it in one English sentence, page included, to show as it is. Shapes with the same problem on one page are one warning, with the count in `message`.\n\n```json\n\"warnings\": [\n  {\"code\": \"pdf_gradient\", \"page\": 1, \"message\": \"Page 1: a shape could not be converted (it is a colour gradient or a pattern).\"},\n  {\"code\": \"font_added\", \"source\": \"google\", \"name\": \"Montserrat Bold\"},\n  {\"code\": \"asset_missing\", \"kind\": \"font\", \"name\": \"Helvetica World\", \"source_id\": \"...\"}\n]\n```\n\nRead `warnings` by `code`, and ignore a code you do not know: more may be added.\n\nA file that is not a readable PDF is rejected with `422` in the standard error envelope, with the reason in `error_description`:\n\n```json\n{\n  \"error\": \"pdf_conversion_failed\",\n  \"error_description\": \"This PDF could not be converted. Upload a PDF exported from Canva, or a Typograph template package.\",\n  \"request_id\": \"...\"\n}\n```\n\nThe conversion is built for Canva's PDF export. A PDF from another tool may convert, but is not supported.","agent_tools":[]},{"title":"File Service","url":"https://docs.typograph.nl/guides/api/file-service#error-responses","section":"Error Responses","audience":"api","headings":[],"text":"Standard envelope (see [API Overview → Error Handling](https://docs.typograph.nl/guides/api/overview#error-handling)):\n\n| Status | Error | Description |\n|--------|-------|-------------|\n| `401` | `unauthorized` | Missing or invalid token |\n| `403` | `access_denied` | Missing scope or resource permission |\n| `403` | `organization_not_bound` | The resource, or the target of a move, belongs to another organization than the one the token reaches ([details](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)) |\n| `403` | `invite_recipient_mismatch` | The invitation was sent to another address than the caller's; `recipient_hint` holds it, masked |\n| `404` | `not_found` | Resource does not exist or not accessible to caller, such as a form of another organization than the token's |\n| `409` | `in_trash` | What the call works on is in the trash, or in a project or team that is; `resource_type`, `resource_id` and `deleted_at` name it ([details](https://docs.typograph.nl/guides/api/file-service#nothing-works-on-what-is-in-the-trash)) |\n| `409` | `container_in_trash` | A restore of something whose project or team is still in the trash; restore that first |\n| `409` | `team_limit_reached` | A new or restored team beyond the plan's number of teams ([details](https://docs.typograph.nl/guides/api/file-service#the-plans-number-of-teams)) |\n| `409` | `editor_seats_taken` | `edit` or `admin` for someone who is no editor yet, or a new team by someone who is no editor yet, while the plan's editor seats are all taken ([details](https://docs.typograph.nl/guides/api/file-service#editor-seats)) |\n| `409` | `admin_inherited` | A role below `admin` on a project or template for someone who is admin of its team or project, by an invitation, a change of one, accepting one or a change of a role; `admin_resource_type` says which ([details](https://docs.typograph.nl/guides/api/file-service#sharing-invites-roles-permissions)) |\n| `409` | `access_request_open` | The caller asked for access to this team, project or template already, and that request is still open ([details](https://docs.typograph.nl/guides/api/file-service#asking-for-access)) |\n| `409` | `access_request_limit_reached` | The caller has 20 open requests for access ([details](https://docs.typograph.nl/guides/api/file-service#asking-for-access)) |\n| `409` | `conflict` | Duplicate slug / concurrent edit |\n| `422` | `validation_error` | Field validation failed |\n| `503` | `subscription_service_unavailable` | Creating or restoring a team, or granting `edit` or `admin`: the organization's plan could not be checked; try again in a moment |\n\n```json\n{\n  \"error\": \"validation_error\",\n  \"error_description\": \"Request validation failed\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\",\n  \"details\": [\n    { \"field\": \"name\", \"code\": \"required\", \"message\": \"The name field is required.\" }\n  ]\n}\n```","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service","section":null,"audience":"api","headings":[],"text":"The Identity Service exposes user identity and organization data to third-party applications through `/v1/identity/*`. Everything here requires a **user token** obtained via the [Authorization Code + PKCE flow](https://docs.typograph.nl/guides/api/authentication#authorization-code-flow-with-pkce).\n\n**Base URL:** `/v1/identity/`\n\n**Scopes:** `identity` (full access), `identity:read`, `identity:write`, and `identity:delete` for deleting the person's own account, or an organization they own, which `identity` does not include.","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#current-user","section":"Current User","audience":"api","headings":["Get current user","Update current user","Delete the current user's account"],"text":"### Get current user\n\n```\nGET /v1/identity/users/me\n```\n\n**Scopes:** `identity` or `identity:read`\n\n```bash\ncurl https://api.typograph.nl/v1/identity/users/me \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nResponse:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"image_url\": \"https://cdn.typograph.nl/avatars/johndoe.png\",\n  \"handle\": \"johndoe\",\n  \"first_name\": \"John\",\n  \"last_name\": \"Doe\",\n  \"email\": \"user@example.com\",\n  \"phone\": null,\n  \"settings\": {\n    \"send_notification_email\": true,\n    \"enable_dark_mode\": false,\n    \"theme\": \"system\",\n    \"language\": \"en\",\n    \"time_zone\": \"system\",\n    \"browser_time_zone\": \"Europe/Amsterdam\"\n  },\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"updated_at\": \"<ISO 8601 timestamp>\",\n  \"last_sign_in_at\": \"<ISO 8601 timestamp>\",\n  \"analytics_consent\": null,\n  \"analytics_consent_updated_at\": null,\n  \"initial\": \"J\",\n  \"full_name\": \"John Doe\"\n}\n```\n\n`theme` is the Typograph portal's theme: `light`, `dark`, or `system` to follow the setting of the\nperson's computer or browser. `enable_dark_mode` is deprecated: it is `true` only while the theme is\n`dark`. Read `theme` instead.\n\n`time_zone` is the zone the Typograph portal and the person's emails show times in: `system` (the\ndefault) follows the person's browser, or an IANA name such as `Europe/London` that the person\nchose. `browser_time_zone` is the zone the person's browser last reported, an IANA name, and\n`null` until one did; emails use it while `time_zone` is `system`.\n\n`analytics_consent` is the person's answer to product analytics in the Typograph portal:\n`granted`, `declined`, or `null` while they have not answered. `analytics_consent_updated_at` is\nwhen that answer last changed, and `null` until the first.\n\n### Update current user\n\n```\nPATCH /v1/identity/users/me\n```\n\n**Scopes:** `identity` or `identity:write`\n\nSend only the fields you want to change: a field the body leaves out keeps its value. The answer\nis the whole profile, as `GET /v1/identity/users/me` returns it.\n\n```bash\ncurl -X PATCH https://api.typograph.nl/v1/identity/users/me \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"first_name\": \"John\", \"last_name\": \"Smith\", \"handle\": \"johnsmith\"}'\n```\n\n| Field | Type | Notes |\n|---|---|---|\n| `handle` | string | Up to 255 characters, not empty |\n| `first_name` | string | Up to 255 characters, not empty |\n| `last_name` | string | Up to 255 characters, not empty |\n| `phone` | string or `null` | Up to 255 characters; `null` removes it |\n| `image_url` | string or `null` | A URL of up to 2000 characters; `null` goes back to the generated avatar |\n| `settings` | object | Any of `send_notification_email` (boolean), `theme` (`light`, `dark`, or `system` to follow the person's device) `language` (`en` or `nl`), `time_zone` (`system` or an IANA time zone name such as `Europe/London`) and `browser_time_zone` (an IANA time zone name). The settings you send are merged into the current ones: `{\"settings\": {\"language\": \"nl\"}}` changes the language only. `enable_dark_mode` (boolean) is deprecated: `true` sets the theme to `dark`, `false` turns `dark` into `light` and leaves `light` or `system` as they are; with a `theme` in the same body, the `theme` wins |\n| `analytics_consent` | string or `null` | `granted` or `declined`: whether the person allows product analytics in the portal. `null` takes it back to unanswered. Change it only when the person asks; the same answer again keeps `analytics_consent_updated_at` |\n\nThe email address does not change through the profile: an `email` in the body is ignored. A\n`null` for a field that always has a value (`handle`, `first_name`, `last_name`, `settings` or\none of the settings) answers `422` `validation_error`, as does a value outside the rules above;\nnothing changes then.\n\n### Delete the current user's account\n\nAn app can delete the signed-in person's own account, but only with a code from that person's\nmailbox. It takes two steps: the app asks Typograph to mail the code, and then sends the code the\nperson gives it.\n\n**Scopes:** `identity:delete`, on a user token. `identity` does not include it: your app asks for\nit on its own, and the person sees it on the consent screen. Every plan includes it.\n\n#### 1. Mail the person their code\n\n```\nPOST /v1/identity/users/me/deletion-code\n```\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/identity/users/me/deletion-code \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nAnswers `204` without a body. The mail, in the person's language, carries a six-digit code that\nworks for 15 minutes, and says plainly that it deletes their account. The code does nothing else:\nit signs nobody in. Asking again within a few seconds answers `429`.\n\n#### 2. Delete with that code\n\n```\nDELETE /v1/identity/users/me\n```\n\n```bash\ncurl -X DELETE https://api.typograph.nl/v1/identity/users/me \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"verification_code\": \"123456\", \"reason\": \"switching_tool\"}'\n```\n\n| Field | Required | Description |\n|---|---|---|\n| `verification_code` | yes | The six-digit code from the mail |\n| `reason` | no | Why the person is leaving, when they said so: `no_longer_needed`, `missing_features`, `too_expensive`, `too_difficult`, `switching_tool` or `other`. Another value is a `422`. There is no free text: ask the person, never guess |\n\nResponse:\n\n```json\n{\n  \"message\": \"Account scheduled for deletion. Your data will be permanently removed in 31 days.\",\n  \"deletion_scheduled_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\nThe account stops working at once: every token of the person is revoked, yours included. It is\nremoved for good at `deletion_scheduled_at`, the first daily removal once 30 days have passed.\nUntil then the person can keep it by signing in again. Everything that belongs to them alone goes\nwith it: their profile and the organizations they are the only member of.\n\nAsk the person for the code; never guess one or use another code. A wrong or expired code answers\n`400` with `invalid_request`, and too many wrong ones lock it for a while (`429`).\n\n#### When the person owns a shared organization\n\nWhile the person owns an organization that has other members, both steps answer `409` with\n`organization_owner_with_members`, and no code is sent. Its `error_description` names the\norganizations and what to do, so you can show it as it is:\n\n```json\n{\n  \"error\": \"organization_owner_with_members\",\n  \"error_description\": \"You own Acme, which has other members. Make another member the owner, or remove the other members, and then delete your account. Need help? See the support guide: https://docs.typograph.nl/guides/support/\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\"\n}\n```","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#all-scopes","section":"All Scopes","audience":"api","headings":[],"text":"```\nGET /v1/identity/oauth/scopes\n```\n\n**Scopes:** `identity` or `identity:read`\n\nEvery scope Typograph knows, as `{ \"data\": [{ \"scope\": \"file\", \"description\": \"Full file access\", \"deprecated_by\": null, \"children\": [\"file:read\", \"file:write\", \"file:delete\"] }] }`. `children` lists the narrower scopes a parent scope includes; a deprecated scope names its replacement in `deprecated_by`. Use it to explain what the `allowed_scopes` of an [app](https://docs.typograph.nl/guides/api/identity-service#apps) mean.","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#scopes-an-organizations-plan-allows","section":"Scopes an Organization's Plan Allows","audience":"api","headings":[],"text":"```\nGET /v1/identity/oauth/scopes/me?organization_id={organizationId}\n```\n\n**Scopes:** `identity` or `identity:read`\n\nReturns the scopes an app may be given for the organization: the ones its subscription plan allows, each with its description, as `{ \"data\": [{ \"scope\": \"file\", \"description\": \"Full file access\", \"deprecated_by\": null, \"children\": null }] }`. Without `organization_id`, or for an organization without a subscription, `data` is empty. Only a member of the organization gets an answer: anyone else gets a `403` with `access_denied`, and another organization than the one the token reaches a `403` with `organization_not_bound` ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)). When the plan cannot be read, the answer is a `503` with `subscription_service_unavailable`. It does not describe the current token: for that, use [token introspection](https://docs.typograph.nl/guides/api/authentication#token-introspection).","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#organizations","section":"Organizations","audience":"api","headings":["List organizations","Create an organization","Update an organization","Delete an organization","Organization members","Transfer ownership","Organization invitations"],"text":"Every OAuth client and every API request is attributed to an organization. Organizations carry the subscription plan and the rate-limit quotas.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations` | POST | `identity` / `identity:write` |\n| `/v1/identity/organizations/{id}` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{id}` | PATCH | `identity` / `identity:write` |\n| `/v1/identity/organizations/{id}/deletion-code` | POST | `identity:delete` |\n| `/v1/identity/organizations/{id}` | DELETE | `identity:delete` |\n\n### List organizations\n\n```bash\ncurl https://api.typograph.nl/v1/identity/organizations \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nReturns the organizations the authenticated user is a member of. Uses standard [pagination](https://docs.typograph.nl/guides/api/pagination).\n\n### Create an organization\n\n`POST /v1/identity/organizations` creates another organization, for another company or a client. The caller becomes its owner, and it starts on the free plan with its own members, apps, limits and billing. Send a `name`; `slug`, `description` and `website_url` are optional. Without a `slug`, one is derived from the name: accents become their plain letter, anything else a hyphen, and `-1`, `-2` is added when the slug is taken. A slug you send that another organization already has is a `409` with `slug_conflict`; a property that fails validation is a `422`. The answer is the new organization, with `role` `owner`.\n\nA person owns one organization on the free plan. Someone who owns one already is refused with a `409` and `free_organization_limit_reached`: a second organization needs a paid plan, which you ask us for (see [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting#one-free-organization-per-person)). When the plan cannot be checked, the answer is a `503` with `subscription_service_unavailable`; try again in a moment.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/identity/organizations \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Press Room\"}'\n```\n\nIt needs `identity` or `identity:write`. An MCP client that registered itself, such as Claude Desktop, does not get `identity:write`, so it cannot create organizations; an app of your organization that holds the scope can.\n\n### Update an organization\n\n`PATCH /v1/identity/organizations/{id}` changes the properties in the body and leaves the rest: `name`, `slug`, `description`, `image_url`, `website_url`, `contact_email` and the `billing_*` details. It takes the `owner` or `admin` role in the organization. A slug another organization already has is a `400` with `slug_conflict`; a property that fails validation is a `422`.\n\n```bash\ncurl -X PATCH https://api.typograph.nl/v1/identity/organizations/$ORG_ID \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"website_url\": \"https://www.example.com\"}'\n```\n\n### Delete an organization\n\nThe owner can delete an organization, confirmed with a code from the owner's mailbox, the way an\naccount is deleted. It takes two steps: the app asks Typograph to mail the code, and then sends the\ncode the person gives it. A person always keeps at least one organization.\n\n**Scopes:** `identity:delete`, on a user token. `identity` does not include it: your app asks for\nit on its own, and the person sees it on the consent screen.\n\n#### 1. Mail the owner their code\n\n```\nPOST /v1/identity/organizations/{id}/deletion-code\n```\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/deletion-code \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nAnswers `204` without a body. The mail, in the owner's language, names the organization, says what\ndeleting it means, and carries a six-digit code that works for 15 minutes. The code deletes this\norganization and nothing else: not another organization, not the account, and it signs nobody in.\nAsking again within a few seconds answers `429` with `verification_code_rate_limit_exceeded`.\n\n#### 2. Delete with that code\n\n```\nDELETE /v1/identity/organizations/{id}\n```\n\n```bash\ncurl -X DELETE https://api.typograph.nl/v1/identity/organizations/$ORG_ID \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"verification_code\": \"123456\"}'\n```\n\nResponse:\n\n```json\n{ \"message\": \"Organization deleted\" }\n```\n\nThe API cannot undo it. Every member loses access at once, the tokens bound to the\norganization are revoked, its webhook subscriptions are deleted, and its subscription ends, a paid\nplan included, without waiting for the end of the period. Its teams, projects, templates, forms\nand files cannot be opened any more, and are deleted for good 30 days later, with the answers\nentered in the forms. Until then, support can restore the organization on the owner's request\n([Deleted it by mistake?](https://docs.typograph.nl/portal/organization/delete-organization#deleted-it-by-mistake)); the\ntokens and webhook subscriptions do not come back. Your other organizations stay as they are.\n\nAsk the person for the code; never guess one or use another code.\n\n| Status | Error | When |\n|--------|-------|------|\n| `400` | `invalid_request` | The code is wrong or expired, or was issued for another organization |\n| `403` | `access_denied` | The caller is not the owner |\n| `404` | `not_found` | The organization does not exist |\n| `409` | `last_organization` | It is the only organization the owner is a member of |\n| `422` | `validation_error` | `verification_code` is missing or not six digits |\n| `429` | `verification_rate_limit_exceeded` | Too many wrong codes: wait, then ask for a new one |\n\n`last_organization` comes before a code is sent and before a code is used up. Its\n`error_description` says what to do, so you can show it as it is:\n\n```json\n{\n  \"error\": \"last_organization\",\n  \"error_description\": \"This is the only organization you are a member of, and you always keep at least one. Create or join another organization first, and then delete this one.\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\"\n}\n```\n\n### Organization members\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations/{id}/members` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/members/{memberId}` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/members/{memberId}` | PATCH | `identity` / `identity:write` |\n| `/v1/identity/organizations/{orgId}/members/{memberId}` | DELETE | `identity` / `identity:write` |\n| `/v1/identity/organizations/{orgId}/members/transfer-ownership` | POST | `identity` / `identity:write` |\n\nThe member list uses standard [pagination](https://docs.typograph.nl/guides/api/pagination): `limit` (default `50`, max `100`) and `offset`, with a `meta` object.\n\nMember object:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n  \"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"user_email\": \"user@example.com\",\n  \"user_name\": \"John Doe\",\n  \"user_image_url\": null,\n  \"role\": \"admin\",\n  \"can_use_quota\": true,\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"invited_by_id\": \"019b28fb-a11e-7641-a28f-e978f892ec05\"\n}\n```\n\n`memberId` in the paths is the membership's `id`, not the user's. Any member can list and read the members; changing them takes the `owner` or `admin` role. There is no route that adds someone: a person becomes a member only by accepting an [invitation](https://docs.typograph.nl/guides/api/identity-service#organization-invitations), so nobody is put into an organization without agreeing to it.\n\n- **Change** (`PATCH .../members/{memberId}`) `role`, `can_use_quota` or both.\n- **Remove** (`DELETE .../members/{memberId}`) answers `{ \"message\": \"Member removed\" }`. Members may also remove themselves.\n\nAn organization has one owner. The owner cannot be demoted or removed (`cannot_demote_owner`, `cannot_remove_owner`), and nobody is made owner with a `PATCH` (`cannot_promote_to_owner`): the owner transfers ownership instead. Only the owner changes or removes the owner.\n\n### Transfer ownership\n\n`POST .../members/transfer-ownership` hands the organization to another member. Only the owner can call it, with the new owner's **user ID** (`user_id`, not the membership's `id`). In one step the new owner becomes `owner` and the previous owner becomes `admin`: they keep managing members and settings, but no longer manage the subscription or delete the organization, and cannot take ownership back themselves.\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/members/transfer-ownership \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\"}'\n```\n\nThe answer holds both memberships, as member objects:\n\n```json\n{\n  \"message\": \"Ownership transferred\",\n  \"new_owner\": { \"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\", \"role\": \"owner\", \"...\": \"...\" },\n  \"previous_owner\": { \"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec05\", \"role\": \"admin\", \"...\": \"...\" }\n}\n```\n\n| Status | Error | When |\n|--------|-------|------|\n| `400` | `invalid_request` | `user_id` is missing or not a UUID |\n| `400` | `not_a_member` | The user is not a member of this organization |\n| `400` | `already_owner` | The user is the owner already, for example the caller |\n| `403` | `access_denied` | The caller is not the owner |\n| `404` | `not_found` | The organization does not exist |\n| `409` | `member_scheduled_for_deletion` | The member is deleting their account and cannot become the owner |\n\nOther services can see the old roles for up to a minute after the transfer.\n\n### Organization invitations\n\nAn owner or admin invites someone by email address. Typograph mails them a link to the portal that works for 7 days; they sign in, or create an account, with that address and join. Someone without a Typograph account can be invited too.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations/{id}/invitations` | POST | `identity` / `identity:write` / `identity:invitations` |\n| `/v1/identity/organizations/{id}/invitations` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{id}/invitations/{invitationId}/resend` | POST | `identity` / `identity:write` / `identity:invitations` |\n| `/v1/identity/organizations/{id}/invitations/{invitationId}` | DELETE | `identity` / `identity:write` / `identity:invitations` |\n| `/v1/identity/users/me/invitations` | GET | `identity` / `identity:read` |\n| `/v1/identity/users/me/invitations/lookup` | POST | `identity` / `identity:read` |\n| `/v1/identity/users/me/invitations/{invitationId}/accept` | POST | `identity` / `identity:write` |\n| `/v1/identity/users/me/invitations/{invitationId}/decline` | POST | `identity` / `identity:write` |\n\nAll of them take a user token. Inviting, listing, sending again and revoking take the `owner` or `admin` role in the organization; anyone else gets a `403` with `access_denied`.\n\nInvitation object:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec10\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"organization_name\": \"Example Print\",\n  \"email\": \"alex@example.com\",\n  \"role\": \"member\",\n  \"invited_by_id\": \"019b28fb-a11e-7641-a28f-e978f892ec05\",\n  \"invited_by_name\": \"John Doe\",\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"sent_at\": \"<ISO 8601 timestamp>\",\n  \"expires_at\": \"<ISO 8601 timestamp>\",\n  \"is_expired\": false\n}\n```\n\n#### Invite someone\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/invitations \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"alex@example.com\", \"role\": \"member\"}'\n```\n\n`role` is `member` (the default) or `admin`. Nobody is invited as `owner`: an organization has one owner, and ownership moves only by a [transfer](https://docs.typograph.nl/guides/api/identity-service#transfer-ownership). The answer is `201` with the invitation, and the mail goes out a moment later. The mail is in the invited person's language when they have an account, otherwise in English.\n\n| Status | Error | When |\n|--------|-------|------|\n| `409` | `already_member` | The address belongs to a member of the organization |\n| `409` | `already_invited` | The address has an invitation already, expired or not: send that one again instead |\n| `409` | `member_limit_reached` | Members and open invitations together are at the plan's limit of team members |\n| `409` | `too_many_invitations` | The organization has 50 open invitations, the most it can have at once |\n| `422` | `validation_error` | `email` is not an email address, or `role` is not `member` or `admin` |\n| `429` | `invitation_recently_sent` | The organization mailed this address less than a minute ago, also when that invitation was revoked since; `Retry-After` says how long to wait |\n| `429` | `invitation_mail_limit` | The address has had 10 invitation mails today, from all organizations, or you have sent 100; `Retry-After` runs until midnight UTC |\n| `503` | `subscription_service_unavailable` | The plan could not be checked; try again in a moment |\n\nEvery open invitation holds a place under the plan's limit, so accepting normally finds room. Only a plan that shrank after the invitation was sent answers the accept with `409 member_limit_reached`.\n\n#### List, send again and revoke\n\n`GET .../invitations` lists the organization's invitations that nobody answered yet, newest first, with standard [pagination](https://docs.typograph.nl/guides/api/pagination). Expired ones are included, with `\"is_expired\": true`.\n\n`POST .../invitations/{invitationId}/resend` mails the invitation again with a new link that works for another 7 days; the link of the earlier mail stops working. That is also how an expired invitation is renewed, which needs room under the plan's limit again (`409 member_limit_reached`). Within a minute of the last mail it answers `429` with `invitation_recently_sent` and a `Retry-After` header, and past the daily limits above with `invitation_mail_limit`.\n\n`DELETE .../invitations/{invitationId}` revokes an invitation (`204`): its link stops working at once.\n\n#### Answer an invitation\n\nThese routes act for the signed-in person, on the invitations sent to their account's email address.\n\n- `GET /v1/identity/users/me/invitations` lists the person's open invitations, with [pagination](https://docs.typograph.nl/guides/api/pagination). Expired ones are left out.\n- `POST /v1/identity/users/me/invitations/lookup` with `{\"token\": \"typ_inv_...\"}` answers the invitation a mail's link points at; the token is the last part of the link. It is sent in the body so it stays out of access logs. A link that was revoked, answered or replaced by a newer mail is a `404`. An expired invitation is answered with `\"is_expired\": true`. When the link was sent to another address, the answer is a `403` with `invite_recipient_mismatch` and a `recipient_hint` such as `a•••@example.com`: sign in with that address.\n- `POST /v1/identity/users/me/invitations/{invitationId}/accept` makes the person a member with the invitation's role and answers the new [member object](https://docs.typograph.nl/guides/api/identity-service#organization-members). The link then stops working. It answers `409` with `invitation_expired` for an expired invitation, `already_member` when the person is a member already, or `member_limit_reached` when the organization's plan has no room left; the invitation is kept in that last case.\n- `POST /v1/identity/users/me/invitations/{invitationId}/decline` removes the invitation (`204`).\n\nAn invitation addressed to someone else is a `404` on accept and decline. Accepting needs `identity:write`, like leaving an organization: it changes the person's memberships. Apps that register themselves through the [MCP server](https://docs.typograph.nl/guides/api/mcp-server), such as Claude Desktop, do not get `identity:write`, so through them a person can list invitations but answers them in the portal.","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#connected-apps","section":"Connected Apps","audience":"api","headings":["Apps of guests","The assistant"],"text":"An organization decides which third-party apps its members may connect. Every app is allowed until an admin sets a policy for it.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations/{orgId}/connected-apps` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/connected-apps/{clientId}` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/connected-apps/{clientId}/policy` | PUT | `identity` / `identity:write` |\n\nAll three take the `owner` or `admin` role. The list holds the apps that have a policy; reading one app without a policy reports it as `allow`. `clientId` is the app's OAuth client ID.\n\n```bash\ncurl -X PUT https://api.typograph.nl/v1/identity/organizations/$ORG_ID/connected-apps/$CLIENT_ID/policy \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"status\": \"block\", \"reason\": \"Not approved for customer data\"}'\n```\n\n`status` is one of:\n\n| Status | Effect |\n|--------|--------|\n| `allow` | Members can authorize the app. |\n| `block` | Members cannot authorize the app, and every token it holds for the organization is revoked at once. The organization's other apps keep working. |\n| `pending_approval` | New authorizations wait for an admin's approval. |\n\nAn app object:\n\n```json\n{\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec10\",\n  \"client_name\": \"Example Connector\",\n  \"client_kind\": \"standard\",\n  \"cimd_url\": null,\n  \"status\": \"block\",\n  \"reason\": \"Not approved for customer data\",\n  \"policy_set_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\nThe `PUT` answers with `client_id`, `status`, `reason` and `policy_set_at`.\n\n### Apps of guests\n\nA guest is someone from another organization with a role in one of your teams. An app a guest connects, such as an AI assistant, is tied to the guest's own organization, so it does not reach your teams. Your admins can let it read them:\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations/{orgId}/guest-apps` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/guest-apps` | PUT | `identity` / `identity:write` |\n\nBoth take the `owner` or `admin` role. `PUT` takes `{\"allow_read\": true}` or `false`, and it is off until you turn it on. While it is on:\n\n- An app of a guest reads the teams, projects and templates its person has a role on here, together with the files a design needs to be drawn: its media, fonts and clips.\n- It never changes anything in your teams, whatever role its person has. Writes are refused with `organization_not_bound`.\n- Its PDFs and other jobs are billed to the guest's own organization.\n- An app you blocked, or hold for approval, under [Connected Apps](https://docs.typograph.nl/guides/api/identity-service#connected-apps) reads nothing.\n\nTurning it off stops the reads within a minute. Both routes answer the setting with the apps that were let read, the latest first. Each app is one of your connected apps, so you block one there:\n\n```json\n{\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"allow_read\": true,\n  \"apps\": [\n    {\n      \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec10\",\n      \"client_name\": \"Example Connector\",\n      \"client_kind\": \"cimd_virtual\",\n      \"cimd_url\": \"https://connector.example.com/oauth/client.json\",\n      \"status\": \"allow\",\n      \"last_guest_read_at\": \"<ISO 8601 timestamp>\"\n    }\n  ]\n}\n```\n\n`last_guest_read_at` is when the app was last let read for a guest, to the hour. `client_kind` and `cimd_url` say where the app comes from, as under [Connected Apps](https://docs.typograph.nl/guides/api/identity-service#connected-apps). An app that registered itself (`cimd_virtual`) chose its `client_name` itself, so two apps can carry one name: the host of `cimd_url` tells them apart.\n\n### The assistant\n\nThe assistant in the Typograph portal works for each person in every organization they belong to, as a member or as a guest. Your owners and admins decide what it may do in your organization, for everyone who works there, and whether guests may spend your organization's AI credits on work in it ([AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant)):\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/organizations/{orgId}/assistant` | GET | `identity` / `identity:read` |\n| `/v1/identity/organizations/{orgId}/assistant` | PUT | `identity` / `identity:write` |\n\nEvery member reads it. `PUT` takes the `owner` or `admin` role, and `ai_access`, `guests_use_credits` or both: what you leave out stays as it is. Both answer:\n\n```json\n{\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"ai_access\": \"read_write\",\n  \"guests_use_credits\": false,\n  \"can_change\": true,\n  \"can_turn_off\": false,\n  \"changed_at\": \"<ISO 8601 timestamp>\",\n  \"is_allowed\": true\n}\n```\n\n- `ai_access`: `read_write` (the default) lets the assistant read and, after a plan the person confirms, change things; `read_only` lets it only read, and refuses every change; `off` keeps it out entirely. A lower setting takes effect within a minute.\n- `guests_use_credits`: whether people with a role on your teams who are not members may use your organization's AI credits for work in it; `false` by default. Editors and admins who are members always may; viewers and commenters never do.\n- `can_change`: whether you may change it, as an owner or admin.\n- `can_turn_off`: whether `off` can be chosen now. `off` is part of the Enterprise plan; an organization that is `off` already keeps it on every plan.\n- `changed_at`: when an owner or admin last changed either setting; null when nobody did.\n- `is_allowed`: deprecated, read `ai_access`. `true` unless `ai_access` is `off`. As input, `false` sets `ai_access` to `off`, and `true` sets `read_write` where the assistant is `off`, and changes nothing otherwise.\n\n`PUT` answers `400` for an unknown `ai_access`, a `guests_use_credits` that is not a boolean, an empty body, or `max_preset`, which is gone: what the assistant may do is now the person's own rights within `ai_access`. It answers `403` `forbidden` to a member who is no owner or admin, and `403` `plan_does_not_allow_ai_off` for `off` on Free or Pro, unless the organization is `off` already.","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#apps","section":"Apps","audience":"api","headings":["List and read apps","App logo"],"text":"An organization's apps are the OAuth clients it builds on Typograph. Any member can list and read them, and an owner or admin can give an app the logo its consent screen shows. Creating an app, changing its name, scopes or redirect URIs, deleting it and regenerating its secret happen in the portal only (see [Managing OAuth Clients](https://docs.typograph.nl/guides/api/identity-service#managing-oauth-clients)), so no answer here carries a client secret.\n\n| Endpoint | Method | Scopes |\n|----------|--------|--------|\n| `/v1/identity/oauth/clients?organization_id={orgId}` | GET | `identity` / `identity:read` |\n| `/v1/identity/oauth/clients/{clientId}` | GET | `identity` / `identity:read` |\n| `/v1/identity/oauth/clients/{clientId}/logo` | PUT | `identity` / `identity:write` |\n| `/v1/identity/oauth/clients/{clientId}/logo` | DELETE | `identity` / `identity:write` |\n\n### List and read apps\n\n```bash\ncurl \"https://api.typograph.nl/v1/identity/oauth/clients?organization_id=$ORG_ID\" \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\nThe list uses standard [pagination](https://docs.typograph.nl/guides/api/pagination): `limit` (default `10`, max `50`) and `offset`, with a `meta` object. Only a member of the organization gets an answer: anyone else gets a `403` with `access_denied`, and another organization than the one the token reaches a `403` with `organization_not_bound` ([which one](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)).\n\nAn app object:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec10\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"name\": \"Print Shop\",\n  \"redirect_uris\": [\"https://shop.example.com/oauth/callback\"],\n  \"allowed_scopes\": [\"file:read\", \"publisher\"],\n  \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n  \"is_confidential\": true,\n  \"is_active\": true,\n  \"allowed_ips\": null,\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"updated_at\": \"<ISO 8601 timestamp>\",\n  \"logo_url\": \"https://api.typograph.nl/v1/identity/oauth/clients/019b28fb-a11e-7641-a28f-e978f892ec10/logo?v=<timestamp>\"\n}\n```\n\n`allowed_ips` lists the addresses and CIDR ranges the app's credentials and tokens work from, or is `null` when any address may; an owner or admin sets it in the portal ([IP allowlist](https://docs.typograph.nl/guides/api/authentication#ip-allowlist)).\n\n`GET /v1/identity/oauth/clients/{clientId}` answers a member of the app's organization with the whole app object, when the token reaches that organization. Anyone else gets only what the consent screen shows: `id`, `name`, `kind`, `cimd_url`, `logo_uri`, `homepage_uri`, `is_first_party` and `logo_url`. `logo_url` is `null` for an app without a logo.\n\n### App logo\n\n```bash\ncurl -X PUT https://api.typograph.nl/v1/identity/oauth/clients/$CLIENT_ID/logo \\\n  -H \"Authorization: Bearer $USER_TOKEN\" \\\n  -H \"Content-Type: image/png\" \\\n  --data-binary @logo.png\n```\n\nThe image is the request body: a **square** PNG, JPEG or WebP of **128 to 1024 pixels per side**, at most **512 KB**. Only an app that signs people in (the `authorization_code` grant) shows a consent screen, so only such an app takes a logo. The answer is the app object with a new `logo_url`. An image that misses a requirement is a `400` with `invalid_logo`, and `error_description` says which one.\n\n`DELETE /v1/identity/oauth/clients/{clientId}/logo` removes the logo and answers `204`; the consent screen shows the placeholder again.\n\nBoth take the `owner` or `admin` role in the app's organization: a plain member gets a `403`, someone outside it a `404`, as does a token that reaches another organization. The portal does the same under **Apps** → **⋯** → **Logo** (see [Authentication → App logo](https://docs.typograph.nl/guides/api/authentication#app-logo)).","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#error-responses","section":"Error Responses","audience":"api","headings":[],"text":"| Status | Error | Description |\n|--------|-------|-------------|\n| `401` | `unauthorized` | Missing or invalid token |\n| `400` | `invalid_request` | The body is not valid JSON, not a JSON object, or holds a key the endpoint does not know, named in `error_description`; or the deletion code is wrong or has expired |\n| `400` | `user_already_deleted` | The account is already scheduled for deletion |\n| `400` | `invalid_logo` | The app logo is refused; `error_description` says why |\n| `403` | `access_denied` | Missing scope for the requested endpoint |\n| `403` | `invalid_token_type` | Client token used on a user-only endpoint |\n| `403` | `forbidden` | `PUT /v1/identity/organizations/{orgId}/assistant`: the caller is a member but no owner or admin of the organization |\n| `403` | `plan_does_not_allow_ai_off` | `PUT /v1/identity/organizations/{orgId}/assistant`: `ai_access` `off` is part of the Enterprise plan |\n| `404` | `not_found` | User, organization, member or app does not exist |\n| `409` | `organization_owner_with_members` | The person owns an organization that has other members: they make another member the owner, or remove the others, before deleting their account |\n| `409` | `free_organization_limit_reached` | `POST /v1/identity/organizations`: the person owns an organization on the free plan already, and a second one needs a paid plan |\n| `422` | `validation_error` | Field validation failed |\n| `429` | `verification_code_rate_limit_exceeded` | A deletion code was sent moments ago |\n| `429` | `verification_rate_limit_exceeded` | Too many wrong deletion codes: locked for a while |\n| `503` | `subscription_service_unavailable` | The organization's plan could not be read: by `GET /v1/identity/oauth/scopes/me`, and by `POST /v1/identity/organizations`, which checks the plans of the organizations the person owns |\n\nStandard error envelope (see [API Overview → Error Handling](https://docs.typograph.nl/guides/api/overview#error-handling)):\n\n```json\n{\n  \"error\": \"validation_error\",\n  \"error_description\": \"Request validation failed\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\",\n  \"details\": [\n    { \"field\": \"name\", \"code\": \"required\", \"message\": \"Name is required.\" }\n  ]\n}\n```","agent_tools":[]},{"title":"Identity Service","url":"https://docs.typograph.nl/guides/api/identity-service#managing-oauth-clients","section":"Managing OAuth Clients","audience":"api","headings":[],"text":"OAuth applications (client ID / client secret) are **created, changed and deleted in the Typograph Portal**, not via the public API: that is also where a secret is shown and regenerated. Through the API you can [list and read them and change their logo](https://docs.typograph.nl/guides/api/identity-service#apps). See [Authentication → Getting Credentials](https://docs.typograph.nl/guides/api/authentication#getting-credentials).","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server","section":null,"audience":"api","headings":[],"text":"Connect an AI agent to Typograph so it can find templates, generate press-ready PDFs and images, convert files, and manage projects and teams. It works for one signed-in user, with that user's rights: it can do what the user can do, and no more.\n\nThe Typograph MCP Server implements the [Model Context Protocol](https://modelcontextprotocol.io/) — the open standard adopted by Anthropic, OpenAI, and Google for connecting AI to external tools and data.\n\n**Production URL:** `https://mcp.typograph.nl`","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#what-ai-agents-can-do","section":"What AI Agents Can Do","audience":"api","headings":["Documentation for agents","What an agent cannot do yet"],"text":"With more than 60 tools available, your AI agents can:\n\n- **Manage content** - Create and organize teams, projects, and templates, or start a template from the gallery\n- **Generate documents** - Create PDFs and images from templates\n- **Convert files** - Transform between file formats\n- **Collaborate** - Share resources, manage permissions, invite users, ask for access and answer requests for it\n- **Run the organization** - List organizations and their members, change an organization's settings, invite people in, change a member's role or remove a member, and hand an organization to another member\n- **Search** - Find projects, templates, and resources\n- **Manage webhooks** - Set up, change and check the webhook subscriptions of your organization\n- **Look after apps** - Read an organization's apps and their scopes, and give an app its consent-screen logo\n- **Look up the documentation** - Search and read these docs, so an agent finds how Typograph works instead of guessing\n\n### Documentation for agents\n\nAn agent can read this documentation in a form made for it, with or without a connection:\n\n- **[`/llms.txt`](https://docs.typograph.nl/llms.txt)** lists every page with its one-line summary ([llms.txt](https://llmstxt.org)).\n- **[`/llms-full.txt`](https://docs.typograph.nl/llms-full.txt)** holds the full text of every page.\n- **[`/docs-index.json`](https://docs.typograph.nl/docs-index.json)** holds the same text cut into one entry per section: `{revision, generated_at, site_url, sections: [{title, url, section, audience, headings, text, agent_tools}]}`.\n- **Through the MCP server**, `docs_search` and `docs_get` search and read the same sections (see [Documentation](https://docs.typograph.nl/guides/api/mcp-server#documentation) below).\n\nAll three files are made from the pages when the site is built, so they never differ from what you read here, and they hold only these public pages. A page names the tools that do what it explains, as `agent_tools`: the [Invites](https://docs.typograph.nl/portal/organization/invites) page, for example, names `organization_invitation_create` and `invitation_create`.\n\n### What an agent cannot do yet\n\n- **Edit a template's content.** No tool changes the elements of a template yet; `template_update` renames it. An agent generates output from a template as the designer saved it, or in batches from CSV data through the [Publisher](https://docs.typograph.nl/guides/api/publisher).\n- **Create or change an app, or see its secret.** Creating an app, changing its name, scopes or redirect URIs, deleting it and regenerating its secret stay in the [portal](https://docs.typograph.nl/guides/api/authentication#getting-credentials): a new app would get your plan's scopes rather than the agent's, and a secret shown to an agent ends up with its model provider.\n- **Be held to a template's roles on the server.** The template's roles and limits are applied by the Viewer in the browser. Enforcing them on every API and MCP call is planned. Until then, connect agents with only the access they need.","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#quick-start","section":"Quick Start","audience":"api","headings":["Let your agent set itself up","Claude Desktop (and other MCP clients)","Direct HTTP (JSON-RPC)"],"text":"Typograph runs the MCP server as a hosted HTTP service at `https://mcp.typograph.nl` — there is nothing to install locally.\n\n### Let your agent set itself up\n\nPaste this into Claude, ChatGPT, Cursor or any other agent:\n\n```\nFetch and follow the instructions at https://docs.typograph.nl/agent-setup/prompt.md to connect me to Typograph.\n```\n\nThe agent reads [the setup instructions](https://docs.typograph.nl/agent-setup/prompt.md), adds the connection for its own app (or walks you through the clicks when it cannot), and checks that the sign-in worked. The portal dashboard offers the same prompt under **Connect your AI assistant**.\n\n### Claude Desktop (and other MCP clients)\n\nPoint your MCP client's `mcpServers` config at the hosted URL:\n\n```json\n{\n  \"mcpServers\": {\n    \"typograph\": {\n      \"url\": \"https://mcp.typograph.nl/mcp\"\n    }\n  }\n}\n```\n\nYour client signs you in on the first tool call: the server answers `401 Unauthorized` and points the client to the Typograph sign-in (see [Authentication](https://docs.typograph.nl/guides/api/mcp-server#authentication)). The client keeps the token and sends it with every request.\n\n### Direct HTTP (JSON-RPC)\n\n```\nPOST https://mcp.typograph.nl/mcp\nContent-Type: application/json\nAuthorization: Bearer <access token>\n\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"team_list\",\n    \"arguments\": {}\n  }\n}\n```","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#authentication","section":"Authentication","audience":"api","headings":["How Authentication Works","Signing out","The 401 response","Required Scopes"],"text":"The MCP server holds no credentials and keeps no sessions. Your MCP client signs in at the Typograph API with OAuth 2.0 (authorization code with PKCE) and sends the access token as `Authorization: Bearer <token>` on every request. The API checks the token on every tool call.\n\n### How Authentication Works\n\n1. **Discovery** - `initialize` and `tools/list` work without a token, so the client can see what the server offers.\n2. **First tool call** - Without a token, the server answers `401 Unauthorized` with a `WWW-Authenticate` header that names the protected resource metadata. The client reads it, finds the Typograph authorization server and opens the sign-in in a browser.\n3. **Every call after** - The client sends the access token, and refreshes it with the refresh token (`offline_access`) when it expires.\n4. **A refused token** - When the token is invalid, expired or revoked, the server answers `401` again, with `error=\"invalid_token\"`, so the client signs in again.\n\n### Signing out\n\nSign out in your MCP client: disconnect or remove Typograph there. There is no tool for it. Revoking a token is the act of the client it was issued to ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)), and the MCP server is not that client: it only passes your token on. An agent also cannot sign you out by accident, or because a document told it to.\n\n### The 401 response\n\n```\nHTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer error=\"invalid_token\", resource_metadata=\"https://mcp.typograph.nl/.well-known/oauth-protected-resource\"\nContent-Type: application/json\n\n{\n  \"jsonrpc\": \"2.0\",\n  \"error\": {\n    \"code\": -32001,\n    \"message\": \"The access token was refused. Sign in again.\",\n    \"data\": {\n      \"type\": \"auth_required\",\n      \"resource_metadata\": \"https://mcp.typograph.nl/.well-known/oauth-protected-resource\"\n    }\n  },\n  \"id\": null\n}\n```\n\nWithout a token, the header is `Bearer resource_metadata=\"...\"` and the message is `Authentication required`.\n\n### Required Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `identity:read` | User profile access, and listing organizations and their members |\n| `identity:delete` | Delete the user's own account, or an organization the user owns, with the code the user receives by email |\n| `file` | Team, project, and template management |\n| `document` | Document validation |\n| `publisher` | PDF and image generation |\n| `converter` | File format conversion |\n| `webhook` | Your organization's webhook subscriptions and their deliveries |\n| `subscription:read` | Subscription and usage info |\n| `offline_access` | Refresh token support |\n\nAn MCP client gets these scopes at most, also when it asks for more in the metadata document it\nidentifies itself with (the JSON document at its `client_id` URL, which clients such as Claude\nDesktop publish), and your organization's plan can narrow them further. `identity:write` is not among them: it also\nchanges member roles and ownership. Tools that need it, such as setting an app's logo, work only\nwith an access token of your own app that holds `identity:write`.","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#available-tools","section":"Available Tools","audience":"api","headings":["Account Management","Organizations","Apps","Teams","Projects","Templates","Pre-made Templates","Media, Fonts & Clips","File Uploads","Document Generation (Publisher)","File Conversion (Converter)","Document Validation","Search","Documentation","Notifications","Invitations & Sharing","Access Control","Asking for Access","Contacts","Favorites & Recent","Trash","Plans","Webhooks"],"text":"### Account Management\n\n| Tool | Description |\n|------|-------------|\n| `identity_whoami` | Get current user profile (ID, email, role, scopes) |\n| `identity_profile_update` | Change the user's own first and last name, phone number and settings (notification emails, the theme: light, dark or system, language) |\n| `identity_account_deletion_code` | Step 1 of deleting the user's own account: email the user a 6-digit code |\n| `identity_account_delete` | Step 2: delete the account with the code the user read from that email, and the optional `reason` the user gave for leaving; answers `deletion_scheduled_at` |\n\nChanging the profile works like the [profile page](https://docs.typograph.nl/portal/workspace/account#updating-your-profile) in the portal. The agent passes only what to change, and the rest keeps its value; an empty phone number clears it, but the first and last name cannot be cleared. The phone number is in international format, starting with `+` and the country code, such as `+31 20 123 4567`, and the language is `en` or `nl`. The name others see follows the first and last name, as in the portal, and the email address does not change here. The tool's description asks the agent to confirm the changes with you first. It needs the `identity` or `identity:write` scope, so a client that signs in on its own is refused and the agent sends you to the portal; an agent connected through an [app of your organization](https://docs.typograph.nl/portal/organization/apps) with that scope can change the profile. See [Update current user](https://docs.typograph.nl/guides/api/identity-service#update-current-user) for the endpoint.\n\nDeleting an account always needs the user. The code goes only to the user's mailbox, so the agent asks the user for it and never guesses one. Without it, nothing is deleted. The optional `reason` is one of `no_longer_needed`, `missing_features`, `too_expensive`, `too_difficult`, `switching_tool` or `other`, passed only when the user said why they are leaving, never guessed. The account stops working at once, and the user can keep it by signing in within 30 days. If the user owns an organization that has other members, both tools answer with what the user has to do first. The endpoints behind them are in the [Identity Service](https://docs.typograph.nl/guides/api/identity-service) reference.\n\n### Organizations\n\n| Tool | Description |\n|------|-------------|\n| `organization_list` | List the organizations you belong to, with your role in each |\n| `organization_create` | Create another organization; you become its owner, on the free plan |\n| `organization_update` | Change an organization's name, slug, description, `website_url`, contact email and billing details |\n| `organization_assistant_get` | Read what the assistant in the Typograph portal may do in the organization (`ai_access`: `read_write`, `read_only` or `off`) and whether guests may use its AI credits (`guests_use_credits`) |\n| `organization_assistant_update` | Set `ai_access`, `guests_use_credits` or both (`off` is part of Enterprise); [asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) and needs `identity:write` |\n| `organization_guest_apps_get` | Read whether the apps of guests may read the organization's teams, and which apps were let read |\n| `organization_guest_apps_update` | Let the apps of guests read the organization's teams, or stop them; [asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) and needs `identity:write` |\n| `organization_member_list` | List an organization's members, with the `member_id` and `user_id` of each |\n| `organization_member_update_role` | Make a member an admin, or an admin a member |\n| `organization_member_remove` | Remove a member from an organization, or leave it yourself |\n| `organization_transfer_ownership` | Make another member the owner; you become an admin |\n| `organization_deletion_code` | Step 1 of deleting an organization you own: email you a 6-digit code |\n| `organization_delete` | Step 2: delete the organization with the code you read from that email |\n| `organization_invitation_create` | Invite an email address into an organization, as a member or an admin; refused with `member_limit_reached` when members and open invitations are at the plan's limit, which `subscription_usage_get` tells beforehand |\n| `organization_invitation_list` | List an organization's invitations that nobody answered yet |\n| `organization_invitation_resend` | Mail an invitation again, with a new link for another 7 days |\n| `organization_invitation_revoke` | Withdraw an invitation; its link stops working |\n| `organization_invitation_list_mine` | List the invitations into organizations sent to your own address |\n| `organization_invitation_accept` | Accept one of your invitations and join the organization |\n| `organization_invitation_decline` | Decline one of your invitations |\n\nAn agent works in **the organization you picked when you connected it**. `organization_list` shows all your organizations, but the other tools reach only that one: for another organization they answer `organization_not_bound`, and the agent tells you to connect again and pick the other organization. Your own invitations (`organization_invitation_list_mine`, accept and decline) are yours, not an organization's, so they work from any connection. See [Which organization a token reaches](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches).\n\nOnly the owner can transfer an organization, whoever calls the tool, and the owner cannot take it back afterwards: only the new owner can transfer it again. The tool [asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) before it acts. The transfer needs the `identity` or `identity:write` scope, which is not among the [scopes above](https://docs.typograph.nl/guides/api/mcp-server#required-scopes) that the MCP server's sign-in asks for. So a client that signs in on its own, such as Claude Desktop, can list organizations and members but not transfer one. An agent that connects through an [app of your organization](https://docs.typograph.nl/portal/organization/apps) with one of those scopes can. See [Transferring Ownership](https://docs.typograph.nl/portal/organization/transfer-ownership) for the same flow in the portal.\n\nChanging a member's role and removing a member follow the same role rules as in the portal: only an owner or admin changes or removes someone else, and an admin never touches the owner. As over the API, any member can also leave by removing their own membership. Nobody is made owner with `organization_member_update_role`, and the owner is never demoted or removed: the owner transfers ownership first. Both tools take the member's `member_id` from `organization_member_list`, not the `user_id`, and both [ask for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) before they act. They need the `identity` or `identity:write` scope as well, so a client that signs in on its own can list the members, but you change and remove them in the portal; an agent connected through an [app of your organization](https://docs.typograph.nl/portal/organization/apps) with that scope can do it. See [Managing Members](https://docs.typograph.nl/portal/organization/members) for the portal, and [Organization members](https://docs.typograph.nl/guides/api/identity-service#organization-members) for the endpoints.\n\nCreating an organization needs the same `identity` or `identity:write` scope, so `organization_create` works only for an agent that connects through such an app. A client that signs in on its own is refused, and the tool's description tells the agent to send you to the portal instead: see [Creating an Organization](https://docs.typograph.nl/portal/organization/create-organization).\n\nChanging an organization's settings follows the portal's rules: only an owner or admin changes them, whoever calls the tool. The agent passes only the settings to change, and the rest keep their values; an empty value clears a setting such as the `website_url` or a billing detail, but never the name, the slug or the contact email, which the portal requires too. The billing country is its two-letter code, such as `NL`. A slug another organization already uses is refused, and the agent asks you for another. The tool's description asks the agent to confirm the changes with you first. It needs the `identity` or `identity:write` scope as well, so a client that signs in on its own is refused and the agent sends you to the organization's **Settings** in the portal; an agent connected through an [app of your organization](https://docs.typograph.nl/portal/organization/apps) with that scope can change them. See [Update an organization](https://docs.typograph.nl/guides/api/identity-service#update-an-organization) for the endpoint.\n\nOnly an owner or admin invites, lists, sends again and revokes invitations. Inviting, sending again and revoking [ask for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) first, so you see the address before a mail goes out. Everything but the two lists needs the `identity` or `identity:write` scope, so a client that signs in on its own can list invitations, but you invite, accept and decline in the portal; an agent connected through an [app of your organization](https://docs.typograph.nl/portal/organization/apps) with that scope can do all of it. Inviting, sending again and revoking also work with the narrower `identity:invitations`, which reaches no other route; the assistant in the portal invites with it. See [Invites](https://docs.typograph.nl/portal/organization/invites) for the portal, and [Organization invitations](https://docs.typograph.nl/guides/api/identity-service#organization-invitations) for the endpoints.\n\nDeleting an organization works like deleting the account: the code goes only to the owner's mailbox, so the agent asks you for it, and without it nothing is deleted. Both tools need `identity:delete`, which the MCP server's sign-in asks for. Only the owner can delete an organization, and never their only one: then the tools answer that you need to create or join another organization first. Neither you nor the agent can undo it (support can restore it within 30 days, see [Deleting an Organization](https://docs.typograph.nl/portal/organization/delete-organization#deleted-it-by-mistake)), and the tool [asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) before it acts. See [Deleting an Organization](https://docs.typograph.nl/portal/organization/delete-organization) for the same flow in the portal, and the [Identity Service](https://docs.typograph.nl/guides/api/identity-service#delete-an-organization) for the endpoints.\n\n### Apps\n\n| Tool | Description |\n|------|-------------|\n| `app_list` | List the apps (OAuth clients) of an organization you are a member of |\n| `app_get` | Get one app by its client ID; never its secret |\n| `app_logo_upload` | Upload or replace an app's consent-screen logo: the image as base64 with its MIME type, a square PNG, JPEG or WebP of 128 to 1024 pixels, at most 512 KB. Takes the `owner` or `admin` role and `identity:write` |\n| `app_logo_delete` | Remove an app's logo. Takes the `owner` or `admin` role and `identity:write` |\n| `scope_list` | List every scope with what it allows: what an app's scopes mean |\n\n### Teams\n\n| Tool | Description |\n|------|-------------|\n| `team_list` | List teams with pagination |\n| `team_get` | Get team details by ID |\n| `team_create` | Create a new team; `organization_id` names the organization it is created in, and is left out for a connection that reaches one organization. Refused with `team_limit_reached` beyond the plan's teams (Free allows one), which `subscription_usage_get` tells beforehand |\n| `team_update` | Update team name |\n| `team_delete` | Soft delete a team |\n| `team_restore` | Restore a deleted team; it counts against the plan's teams again |\n\n### Projects\n\n| Tool | Description |\n|------|-------------|\n| `project_list` | List projects in a team |\n| `project_list_shared` | List projects shared with you |\n| `project_get` | Get project details |\n| `project_create` | Create a new project |\n| `project_update` | Update project name |\n| `project_delete` | Soft delete a project |\n| `project_restore` | Restore a deleted project |\n\n### Templates\n\n| Tool | Description |\n|------|-------------|\n| `template_list` | List templates in a project |\n| `template_list_shared` | List templates shared with you |\n| `template_get` | Get template details |\n| `template_fields` | What a row of data can fill in a template: its merge fields and the JSON Schema of a row ([Merge fields](https://docs.typograph.nl/guides/api/file-service#merge-fields)) |\n| `template_create` | Create a new template |\n| `template_update` | Update template name |\n| `template_touch` | Update modification timestamp |\n| `template_delete` | Soft delete a template |\n| `template_restore` | Restore a deleted template |\n| `template_publish` | Make a PDF or image of a stored template, billed to the organization that owns it ([Publishing a stored template](https://docs.typograph.nl/guides/api/file-service#publishing-a-stored-template)); [asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) |\n| `template_job_get` | Read the status and the download links of a job `template_publish` started |\n\n### Pre-made Templates\n\nThe portal shows a gallery of ready-made designs under a project's **New template** and on **Recents**. An agent can browse it and start from one, as you do in [Starting From the Gallery](https://docs.typograph.nl/portal/content/templates#starting-from-the-gallery).\n\n| Tool | Description |\n|------|-------------|\n| `premade_template_list` | List the gallery, featured templates first. Filter with `category` (a category's `code`) and `is_featured`. Each template says whether it is usable, in `is_usable` |\n| `premade_template_list_categories` | List the gallery's categories, with the `code` to filter on; `has_templates` leaves out the empty ones |\n| `premade_template_get` | Get one pre-made template by its `code` |\n| `premade_template_use` | Copy a pre-made template into a project (`project_id`, `code`). Answers the new template, which is the project's own from then on |\n\n`premade_template_use` needs edit rights on the project and the `file` or `file:write` scope; browsing needs `file` or `file:read`. A template whose `is_usable` is `false` cannot be used yet, and the tool says why. The endpoints behind these tools are in the [File Service](https://docs.typograph.nl/guides/api/file-service#premade-templates) reference.\n\n### Media, Fonts & Clips\n\n| Tool | Description |\n|------|-------------|\n| `media_list` | List media assets in a project, with their tags and, for an image, its `width` and `height` in pixels; `tag_ids` with `tag_match` (`all` or `any`), `untagged`, `search` and `sort` narrow and order it |\n| `media_tag` | Add tags to media and remove them, by name, on up to 100 media at once; a new name makes the tag |\n| `tag_list` | List the tags of a project's media, each with `media_count` |\n| `tag_rename` | Rename a tag; a name another tag of the project has merges the two |\n| `tag_delete` | Delete a tag from every media file that has it |\n| `media_get_urls` | Get presigned download URLs for media; `include_original` adds the file as uploaded, `include_download` a link that saves it under the media's name |\n| `media_delete` | Move a media file to the trash |\n| `media_restore` | Restore a media file from the trash |\n| `font_list` | List fonts in a project |\n| `font_update` | Change a font's display name, its `label` |\n| `font_get_urls` | Get presigned download URLs for fonts |\n| `font_delete` | Move a font to the trash |\n| `font_restore` | Restore a font from the trash |\n| `clip_list` | List the clips in a project; `search` matches the name. What is in the trash: `trash_list` |\n| `clip_delete` | Move a clip to the trash |\n| `clip_restore` | Restore a clip from the trash |\n\nEach takes the `project_id` and the item's ID (`media_id`, `font_id` or `clip_id`; the tag tools a `tag_id`, `media_tag` its `media_ids`). Tagging, renaming and deleting a tag need edit rights on the project, as in the portal. Moving a file or clip to the trash needs edit rights on its project, as in the portal; it stays restorable for 30 days, then it is deleted for good. See [Trash](https://docs.typograph.nl/portal/workspace/trash) for what restoring brings back.\n\n### File Uploads\n\n| Tool | Description |\n|------|-------------|\n| `upload_list` | List upload jobs |\n| `upload_create` | Create upload job and get presigned URL |\n| `upload_process` | Start processing an uploaded file |\n| `upload_get` | Get one upload, to follow it until its `status` is `success` or `failed` |\n\n### Document Generation (Publisher)\n\n| Tool | Description |\n|------|-------------|\n| `publisher_job_list` | List generation jobs with status |\n| `publisher_job_get` | Get job details by ID |\n| `publisher_job_create` | Generate a PDF or images from a template: the template and manifest URLs, the upload URL, the output format, and an optional CSV to merge, with its columns, rows and delivery |\n| `publisher_job_files` | Get output files from completed job |\n| `merge_validate` | Check a merge before printing it: the template, manifest and CSV URLs, the merge's options and `check_overflow_rows`, the rows to lay out to find text that does not fit; answers what [Checking a merge before printing](https://docs.typograph.nl/guides/api/publisher#checking-a-merge-before-printing) describes, `is_valid` and `plan.is_proof` included |\n\n**Example: Generate a PDF**\n\n```json\n{\n  \"name\": \"publisher_job_create\",\n  \"arguments\": {\n    \"template_url\": \"https://your-server.com/templates/invoice.json\",\n    \"manifest_url\": \"https://your-server.com/templates/manifest.json\",\n    \"upload_url\": \"https://your-bucket.example.com/invoice.pdf?X-Amz-Signature=...\",\n    \"output_format\": \"pdf\"\n  }\n}\n```\n\n| Argument | Required | Description |\n|----------|----------|-------------|\n| `template_url` | Yes | The Typograph template JSON |\n| `manifest_url` | Yes | The template's manifest JSON, which names its fonts and images |\n| `upload_url` | Yes | Where the publisher uploads the result, with an HTTP `PUT` of the file: a presigned storage URL, or an endpoint of your own (see [Upload Request Format](https://docs.typograph.nl/guides/api/publisher#upload-request-format)) |\n| `output_format` | No | `pdf` (default), or an image per page: `jpg`, `jpeg`, `png` or `webp` |\n| `csv_url` | No | A CSV file to merge: one document per row, uploaded as one file ([Batch Processing with CSV](https://docs.typograph.nl/guides/api/publisher#batch-processing-with-csv)) |\n| `mapping` | No | With `csv_url`: which column fills which field, `{\"column name\": \"field key\"}` ([Choosing columns and rows](https://docs.typograph.nl/guides/api/publisher#choosing-columns-and-rows)) |\n| `rows` | No | With `csv_url`: the rows to print, `\"3\"` or `\"1-10\"` |\n| `merge_mode` | No | With `csv_url`: `single` (one PDF) or `per_row` (a ZIP with a file per row) ([One file for the merge](https://docs.typograph.nl/guides/api/publisher#one-file-for-the-merge)) |\n| `filename` | No | With `merge_mode` `per_row`: each row's file name, with field keys in `{{…}}` |\n| `on_overflow` | No | With `csv_url`: `fail` (the default) or `warn`, to print a row whose text does not fit, cut off |\n\nEvery URL must be `https` on a public host. The tool sends the publisher's default configuration (300 dpi, RGB). For CMYK, bleed, crop marks or a template bundle, call the [Publisher API](https://docs.typograph.nl/guides/api/publisher) directly.\n\n### File Conversion (Converter)\n\n| Tool | Description |\n|------|-------------|\n| `converter_job_list` | List conversion jobs |\n| `converter_job_get` | Get conversion job details |\n| `converter_job_create` | Convert a file: the source URL and its format, the format to convert to, and the upload URL |\n| `converter_job_files` | Get output files from completed job |\n\n**Example: Convert an image**\n\n```json\n{\n  \"name\": \"converter_job_create\",\n  \"arguments\": {\n    \"source_url\": \"https://your-server.com/photos/cover.tiff\",\n    \"source_format\": \"tiff\",\n    \"target_format\": \"png\",\n    \"upload_url\": \"https://your-bucket.example.com/cover.png?X-Amz-Signature=...\"\n  }\n}\n```\n\nAll four arguments are required. The tool makes these conversions:\n\n| From `source_format` | To `target_format` |\n|---------------------|-------------------|\n| `indesign_package`, `pod_package` | `typograph_package` |\n| `pdf`, or `ai` saved with PDF compatibility | `typograph_package`, see [Convert a PDF](https://docs.typograph.nl/guides/api/converter#convert-a-pdf) |\n| A font: `eot`, `otf`, `ttf`, `woff`, `woff2`, `ttc` | `ttf`, `otf`, `woff`, `woff2`, or `font_variants` |\n| An image: `bmp`, `ai`, `eps`, `gif`, `psd`, `jpg`, `jpeg`, `svg`, `png`, `webp`, `tiff`, `tif` | `jpg`, `png`, `webp`, `svg` (an `ai` file not to `jpg` or `webp`), or `image_variants` |\n| A zip of fonts: `font_package` | `font_variants` |\n| A zip of images: `image_package` | `image_variants` |\n\n`font_variants` and `image_variants` are a zip of every file a store serves for the font or the image. The converter uploads the result to `upload_url` with an HTTP `PUT`, as the publisher does. A combination it cannot make is refused with a `bad_request` that says why. For image sizes, fit modes and the other options, see the [Converter API](https://docs.typograph.nl/guides/api/converter).\n\n### Document Validation\n\n| Tool | Description |\n|------|-------------|\n| `document_validate` | Validate a document template against schema; answers `has_violations`, `violation_count` and the `violations`, as the [Document Service](https://docs.typograph.nl/guides/api/document) names them. Optional `template_headers` for a template behind authentication: they pass through your MCP client and its model, so prefer a short-lived presigned URL for anything sensitive |\n\n### Search\n\n| Tool | Description |\n|------|-------------|\n| `search` | Search the templates, projects, teams, media and fonts the user can open, by name, `query` 2 to 255 characters, or by a filter alone with `query` left out (`types` with `min_updated_at` lists what changed since then, and with `max_updated_at` too what changed in a period that has ended, such as last month); narrow it with `types`, `team_ids` and `project_ids` (in either), `min_updated_at` and `max_updated_at` (before, not at, that moment), add the trash with `include_deleted`; answers a count per type (`template_count`, `media_count`, …) beside the matches |\n\n**Example: Search templates**\n\n```json\n{\n  \"name\": \"search\",\n  \"arguments\": {\n    \"query\": \"invoice\",\n    \"types\": [\"templates\"]\n  }\n}\n```\n\n### Documentation\n\n| Tool | Description |\n|------|-------------|\n| `docs_search` | Search this documentation by keyword: `query` (a few words, at most 200 characters), optionally `audience` (`portal`, `shop` or `api`) and `limit` (1-20, default 5). Answers `results`, the best sections first, each with `title`, `url`, `section`, a `snippet` and `agent_tools`, the tools that do what the section explains |\n| `docs_get` | Read a page as Markdown by the `url` that `docs_search` answered, or one section when the `url` ends in its `#anchor`. Answers `title`, `url`, `markdown` and `agent_tools` |\n\nBoth only read public pages: they need a signed-in connection, like every tool, but no scope.\n\n**Example: How do I invite someone?**\n\n```json\n{\n  \"name\": \"docs_search\",\n  \"arguments\": {\n    \"query\": \"invite someone to a project\",\n    \"audience\": \"portal\"\n  }\n}\n```\n\n### Notifications\n\n| Tool | Description |\n|------|-------------|\n| `notification_list` | List user notifications |\n| `notification_mark_read` | Mark notification as read |\n| `notification_mark_unread` | Mark notification as unread |\n| `notification_mark_all_read` | Mark all notifications as read |\n\n### Invitations & Sharing\n\n| Tool | Description |\n|------|-------------|\n| `invitation_list_resource` | List the pending invitations to a resource |\n| `invitation_list_my` | List the invitations sent to you |\n| `invitation_create` | Invite up to 20 email addresses at once (`emails`), with one `permission` and an optional `message`; every address must be valid before any is sent, and the result lists the `invitations` sent and the addresses `refused`, with the code and message of each. `edit` or `admin` takes one of the plan's editor seats |\n| `invitation_accept` | Accept an invitation (`invitation_id`); one to `edit` or `admin` waits while the editor seats are taken |\n| `invitation_decline` | Decline an invitation (`invitation_id`) |\n| `invitation_delete` | Delete (revoke) a pending invitation (`invitation_id`) |\n\nA resource is a `team`, `project` or `template`; a permission is `view`, `comment`, `edit` or `admin`.\n\nAn admin of a team stays admin of every project and template in it, and an admin of a project of every template in it. `invitation_create`, `invitation_accept` and `role_update` refuse to give such an admin less there: the API refuses it with `409` and `admin_inherited`, and the tool answers the error `bad_request` with the API's message, such as \"ada@example.com is admin of the team this project is in, and stays admin here. Make them admin here, or leave their access as it is.\" Editors and below can be given less on a project or template than on its team.\n\n`edit` and `admin` take one of the plan's [editor seats](https://docs.typograph.nl/guides/api/file-service#editor-seats), `view` and `comment` never do; Free has one seat, the owner's. When the seats are all taken the tool answers the error `editor_seats_taken`, and when the plan allows no more teams `team_limit_reached`, each with the API's message, such as \"Free allows one team. Request another plan, or delete a team first.\", and the agent tells you what you can do. When the plan cannot be checked it answers `service_unavailable`: try again in a moment.\n\n### Access Control\n\n| Tool | Description |\n|------|-------------|\n| `permission_get` | Your own access to a team, project or template: your role, where it is given (`granted_on`), the role from above when this place differs (`inherited_permission`), when it ends (`expires_at`) and the admins to ask for more |\n| `role_list` | List roles for a resource |\n| `role_update` | Update user/team role; raising someone to `edit` or `admin` takes an editor seat |\n| `role_delete` | Remove user/team from resource |\n\n### Asking for Access\n\n| Tool | Description |\n|------|-------------|\n| `access_request_create` | Ask the admins of a team, project or template you can open for a higher `permission` there (`comment`, `edit` or `admin`), with an optional `message` of up to 500 characters |\n| `access_request_list` | The open requests on a team, project or template that you may answer; without `resource_type` and `resource_id`, your own open requests |\n| `access_request_approve` | Give the requester the role they asked for, or the `permission` you pass instead; returns their role |\n| `access_request_decline` | Decline a request; the requester is notified |\n| `access_request_withdraw` | Withdraw your own request |\n\nEach takes the `resource_type` and `resource_id` of the place (optional on `access_request_list`, which without them lists your own), and the answering tools and `access_request_withdraw` the `access_request_id` from `access_request_list`. The rules are those of the portal and the [API](https://docs.typograph.nl/guides/api/file-service#asking-for-access): you can ask only where you can open the place already, once per place (a second request answers `bad_request` with the API's message for `access_request_open`: withdraw the first), and for no more than 20 places at a time. A request stays open for 30 days; the admins are notified when it is sent, and the requester when it is answered or lapses.\n\nTo answer a request you need `edit` on the place, for a role up to `edit` given to someone who holds no role on the place itself; giving `admin`, or raising a role the requester already holds there, takes an admin. `access_request_list` lists only the requests you may answer. `edit` and `admin` take one of the plan's [editor seats](https://docs.typograph.nl/guides/api/file-service#editor-seats): when they are all taken, `access_request_approve` answers `editor_seats_taken` with the API's message. A role no higher than the requester holds by now is refused the same way.\n\n### Contacts\n\n| Tool | Description |\n|------|-------------|\n| `contact_list` | List team contacts |\n\n### Favorites & Recent\n\n| Tool | Description |\n|------|-------------|\n| `favorite_list` | List favorite projects/templates |\n| `favorite_toggle` | Toggle favorite status |\n| `recent_templates` | List the templates the user opened most recently; a template changed without being opened since is not in it (for that, `search` with `min_updated_at`) |\n| `recent_searches` | List recent search queries |\n\n### Trash\n\n| Tool | Description |\n|------|-------------|\n| `trash_list` | List what is in the trash: teams, projects, templates, clips, media and fonts, each kind in its own list, at most 50 of each per page, and each item with `can_restore`: whether the user may restore it |\n\nRestore an item whose `can_restore` is `true` with the restore tool of its kind: `team_restore`, `project_restore`, `template_restore`, `clip_restore`, `media_restore` or `font_restore`. A team takes a team admin, a project or template its admin, and a clip, media file or font anyone who can edit its project. The templates and files of a deleted project are not listed on their own; restoring the project brings them back.\n\n### Plans\n\nThe plans an organization can be on and what each allows, so an agent can say what another plan would give, and how much of its plan the organization uses, so it can say a limit is reached before it asks you for the details of something the plan does not allow. Read-only. A plan is changed by asking Typograph, not through an agent.\n\n| Tool | Description |\n|------|-------------|\n| `subscription_plan_list` | List the plans (Free, Pro, Enterprise) in order, each with `code`, `name`, `description`, `price_monthly` and `price_yearly` (`null`: on request), `currency`, `max_teams` and `editor_seats` (`null`: no limit; the tool lifts them out of the API's `features`), and `quotas` |\n| `subscription_plan_get` | Get one plan by its `code`, in the same shape; a custom or unknown plan answers `not_found` |\n| `subscription_usage_get` | How much the organization `organization_id` uses of its plan: its `tier`, `plan_usage` with `teams`, `editor_seats` and `members` (open invitations included), each `used` (`null` when they could not be counted just now) and `limit` (`null`: no limit) as [`GET /v1/subscription/usage`](https://docs.typograph.nl/api-reference) answers them, and `is_reached`, which the tool adds: `true` once `used` is at the `limit` (a team in the trash does not count), and this month's `usage` against `monthly_limits`, such as `ai_credits` and `storage` in bytes. Only the organization the connection is for: another answers `organization_not_bound` |\n\nAll three need `subscription:read`, which the MCP server's sign-in asks for.\n\n### Webhooks\n\nThe [webhook subscriptions](https://docs.typograph.nl/guides/api/webhooks) of your organization and their deliveries: the same calls as the Webhook Management API, made with the signed-in user's rights.\n\n| Tool | Description |\n|------|-------------|\n| `webhook_list` | List webhook subscriptions; `client_id` lists those of one OAuth client |\n| `webhook_get` | Get a webhook subscription by ID |\n| `webhook_create` | Create a subscription for an https endpoint, with its event types and up to 5 custom headers |\n| `webhook_update` | Change the name, endpoint URL, headers or event types, or switch deliveries on or off with `is_active` |\n| `webhook_delete` | Delete a subscription for good, with its delivery history |\n| `webhook_rotate_secret` | Give a subscription a new signing secret; `revoke_previous` stops the old one at once |\n| `webhook_list_deliveries` | List the deliveries of a subscription, with status, `attempt_count` and the HTTP status your endpoint answered |\n| `webhook_retry_delivery` | Send a delivery that has not arrived (`pending`, `failed` or `retrying`) to its endpoint again, now, with the same event; a `delivered` one is refused, and one delivery is retried at most once a minute ([Retry a Failed Delivery](https://docs.typograph.nl/guides/api/webhooks#retry-a-failed-delivery)) |\n| `webhook_list_event_types` | List the event types a subscription can subscribe to |\n\n`webhook_create` and `webhook_rotate_secret` return the subscription's signing secret, as `secret` and `standard_webhooks_secret` (see [Secret Format](https://docs.typograph.nl/guides/api/webhooks#secret-format)). It is shown once, in that result only, and it passes through your MCP client and its model: have the agent store it with the integration it sets up straight away. No other tool returns it. If it is lost, rotate it; if it may have leaked, rotate it with `revoke_previous`, since a plain rotate keeps the previous secret signing for 24 hours.\n\nEndpoint header values come back masked as `***`, and `webhook_update` replaces all headers at once: a header given the value `***` keeps the value it has, and an empty object `{}` removes them all. Header values you give pass through your MCP client and its model too.\n\nThe webhook tools need a token with the `webhook` scope, or its parts `webhook:read`, `webhook:write` and `webhook:delete`; without them a call answers `forbidden`. The MCP server's sign-in asks for `webhook` (see [Required Scopes](https://docs.typograph.nl/guides/api/mcp-server#required-scopes)). `webhook_create` takes the `client_id` of an OAuth client of your organization, which receives the job events of the jobs it starts.\n\n`webhook_create`, `webhook_update`, `webhook_delete` and `webhook_rotate_secret` [ask for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) before they act. Every tool also carries [annotations](https://docs.typograph.nl/guides/api/mcp-server#tool-annotations) in `tools/list`, so your MCP client can ask you before `webhook_retry_delivery` runs too.","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation","section":"Tools that ask for confirmation","audience":"api","headings":["The flow"],"text":"33 tools act only once the user confirms: those that delete something or move it to the trash, take away or hand over access, email someone or send data to another address, or use billed capacity.\n\n| Why | Tools |\n|-----|-------|\n| Delete or move to the trash | `app_logo_delete`, `clip_delete`, `font_delete`, `media_delete`, `tag_delete`, `tag_rename` (it merges into a tag of the same name), `project_delete`, `team_delete`, `template_delete`, `role_delete`, `invitation_delete`, `webhook_delete`, `organization_delete`, `identity_account_delete` |\n| Take away or hand over access | `organization_assistant_update`, `organization_guest_apps_update`, `organization_member_remove`, `organization_member_update_role`, `organization_transfer_ownership`, `organization_invitation_revoke`, `role_update`, `access_request_approve`, `access_request_decline`, `webhook_rotate_secret` |\n| Email someone, or send data to another address | `invitation_create`, `organization_invitation_create`, `organization_invitation_resend`, `access_request_create`, `webhook_create`, `webhook_update` |\n| Use billed capacity | `publisher_job_create`, `converter_job_create`, `template_publish` |\n\nIn `tools/list` these tools take an optional `confirmation_token` argument, and their description ends with how the confirmation works. The answer to `initialize` carries `instructions` that say the same, for the model behind your client.\n\n### The flow\n\n1. **Call the tool without `confirmation_token`.** It changes nothing: it only reads, with your token, the names of what it would change. It answers, as the text of its content item:\n\n   ```json\n   {\n     \"status\": \"confirmation_required\",\n     \"summary\": \"Delete the project \\\"Brochures\\\" (…7a8b9c0d)\",\n     \"steps\": [\"Move the project \\\"Brochures\\\" (…7a8b9c0d) to the trash; project_restore brings it back\"],\n     \"confirmation_token\": \"Jc0t…\",\n     \"expires_at\": \"<ISO 8601 timestamp>\"\n   }\n   ```\n\n   `summary` says in one line what the call would do, and `steps` what happens, in order, both with the call's own arguments filled in. An argument that is the ID of a team, project, template, organization, member, invitation, access request, role, app, webhook subscription, clip, font or tag is shown by its name, in quotes, followed by the last eight characters of its ID, so two of the same name still look different: `\"Marketing\" (…3f9c2a1b)`. Where that name cannot be read with your token (it is not found, or not yours to see), the whole ID stays. A media file is shown by its ID. A URL in them is shown without its credentials (user name and password, query string, fragment).\n\n   They are in English, or in Dutch when your request asks for it with the `Accept-Language` header (`Accept-Language: nl`): the first language of the header, by weight, that the server writes. Values that are words follow: `\"Nodig joe@example.com uit voor het project \\\"Brochures\\\" (…7a8b9c0d) als Bewerker\"`. Names, addresses and URLs stay as they are.\n2. **Show the summary and the steps to the user**, and wait for their answer.\n3. **Only when the user agrees, call the tool again with the same arguments plus `confirmation_token`.** That call runs the tool and answers as it always does. If the user declines, do nothing: the token expires by itself.\n\nThe token is bound to:\n\n- **the tool** — a token for `project_delete` does not confirm `team_delete`;\n- **its arguments** — every argument, with its default where the call leaves it out, in any order; change one and the token no longer fits;\n- **the caller** — the bearer token the first call was made with.\n\nIt is valid for **5 minutes** and **once**: presenting it uses it up, whether it fits or not. A token that does not fit, is used or has expired changes nothing, and answers:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"confirmation_invalid\",\n    \"message\": \"The confirmation_token is unknown, used, expired, or was issued for other arguments. Nothing was done. Call the tool again without confirmation_token to ask the user again.\"\n  }\n}\n```\n\nCall the tool again without the token to ask the user again. When confirmation cannot be checked just now, the tool answers `service_unavailable` and does nothing either.\n\nThe confirmation comes before the API is asked to act: the first call only reads names. So it answers `confirmation_required` even when the user may not do what it asks; the confirmed call then answers the API's refusal, such as `forbidden`. A call without a bearer token gets the [401 response](https://docs.typograph.nl/guides/api/mcp-server#the-401-response) as any tool call does.\n\n:::caution The confirmation is as strong as your client\nThe server issues the token, but your client decides when to send it back. Keep that decision with the user: send the token only after the user agreed, never because the model decided to on its own, and never because a document, a web page or a tool result asks for it. A client that hands the answer to its model relies on that model to wait for the user.\n:::","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#tool-annotations","section":"Tool annotations","audience":"api","headings":[],"text":"Every tool carries [MCP annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools) in `tools/list`, so your client can decide how to treat it before it runs:\n\n| Annotation | Says |\n|------------|------|\n| `title` | A short, human-readable name, such as \"Move a project to the trash\" |\n| `readOnlyHint` | `true` when the tool only reads |\n| `destructiveHint` | `true` when the tool can remove or overwrite something |\n| `idempotentHint` | `true` when calling it again with the same arguments has no further effect |\n\n```json\n{\n  \"name\": \"project_delete\",\n  \"annotations\": {\n    \"title\": \"Move a project to the trash\",\n    \"readOnlyHint\": false,\n    \"destructiveHint\": true,\n    \"idempotentHint\": false\n  }\n}\n```\n\nThe annotations are hints for your client; [the confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) is what the server enforces.","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#http-endpoints","section":"HTTP Endpoints","audience":"api","headings":[],"text":"| Endpoint | Description |\n|----------|-------------|\n| `POST /mcp` | JSON-RPC 2.0 endpoint for tool calls |\n| `OPTIONS /mcp` | CORS preflight, for clients in a browser |\n| `GET /.well-known/oauth-authorization-server` | OAuth server metadata (RFC 8414) |\n| `GET /.well-known/oauth-protected-resource` | Protected resource metadata (RFC 9728) |\n| `GET /health` | Health check endpoint |\n\nA request body to `POST /mcp` may hold a JSON-RPC batch, a list of requests, of at most 20. A longer batch is answered `400` with the JSON-RPC error `-32600`, `Invalid Request: a batch holds at most 20 requests`, and none of its requests runs.","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#oauth-discovery","section":"OAuth Discovery","audience":"api","headings":[],"text":"The MCP server implements OAuth 2.0 metadata discovery:\n\n```bash\ncurl https://mcp.typograph.nl/.well-known/oauth-authorization-server\n```\n\n**Response:**\n\n```json\n{\n  \"issuer\": \"https://api.typograph.nl\",\n  \"authorization_endpoint\": \"https://api.typograph.nl/oauth/authorize\",\n  \"token_endpoint\": \"https://api.typograph.nl/oauth/token\",\n  \"scopes_supported\": [\"identity:read\", \"identity:delete\", \"file\", \"document\", \"publisher\", \"converter\", \"webhook\", \"subscription:read\", \"offline_access\"],\n  \"response_types_supported\": [\"code\"],\n  \"grant_types_supported\": [\"authorization_code\", \"refresh_token\"],\n  \"code_challenge_methods_supported\": [\"S256\"]\n}\n```","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#response-format","section":"Response Format","audience":"api","headings":["Success Response","Error Response"],"text":"A tool answers a `tools/call` with one text content item. Its text is the JSON below.\n\n### Success Response\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"items\": [\n      {\n        \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n        \"name\": \"Marketing\",\n        \"created_at\": \"<ISO 8601 timestamp>\"\n      }\n    ],\n    \"total\": 1,\n    \"page\": 1,\n    \"page_size\": 20,\n    \"total_pages\": 1\n  }\n}\n```\n\nA list tool answers `items` with the page fields; any other tool answers the resource itself under `data`, with the field names the API sends (`is_active`, `is_expired`, `has_more`, `can_use_quota`, …).\n\nA tool that only acts (delete, restore, touch, accept, decline, revoke, remove, mark, mail a code) answers `success: true` with the ID it acted on, and no flag of its own that repeats it: `{\"success\": true, \"data\": {\"template_id\": \"…\"}}`. `notification_mark_all_read` answers `data: null`.\n\nA [tool that asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) answers its first call with `\"status\": \"confirmation_required\"` instead, and no `success`.\n\n### Error Response\n\nWhen the API refuses a call, the tool answers an error with a fixed code and message. It never passes on the details of the API:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"not_found\",\n    \"message\": \"The requested resource was not found.\"\n  }\n}\n```\n\n| Code | When |\n|------|------|\n| `forbidden` | The token does not allow the action, for example a missing scope |\n| `not_found` | The resource does not exist, or the user cannot see it |\n| `team_limit_reached`, `editor_seats_taken`, `member_limit_reached` | The organization's plan allows no more teams, editor seats or members; the message says what the user can do: request another plan, or delete or free something first |\n| `bad_request` | The API refused the request, as invalid or because of what is in the way (any other `409`); the message says why when the API did |\n| `validation_error` | An argument was refused before the call, for example an empty name or a URL that is not https |\n| `rate_limited` | Too many requests; the message says when to try again when the API did |\n| `organization_off`, `organization_read_only`, `organization_required`, `route_closed_to_assistant` | Met only by the assistant in the Typograph portal: the organization keeps the assistant out or lets it only read ([AI Assistant Settings](https://docs.typograph.nl/portal/organization/assistant)), a tool without a resource needs `organization_id`, or the action is for the person to do in the portal |\n| `confirmation_invalid` | The `confirmation_token` is unknown, used, expired, or for another tool, other arguments or another caller; nothing was done ([confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation)) |\n| `service_unavailable` | The API could not answer, or confirmation could not be checked |\n| `error` | Anything else |\n\nA refused token is no tool error: it is the [401 response](https://docs.typograph.nl/guides/api/mcp-server#the-401-response).","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#example-workflows","section":"Example Workflows","audience":"api","headings":["Generate a Document","Upload and Use Media","Set Up a Webhook","Share a Project"],"text":"A step with a [tool that asks for confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation) takes two calls: the first answers `confirmation_required`, the second, with the user's consent, runs it.\n\n### Generate a Document\n\n```\n1. team_list → Get available teams\n2. project_list → List projects in team\n3. template_list → Find the template\n4. publisher_job_create → Ask the user to confirm (confirmation_required), then create\n   the job with the template, manifest, upload URLs and the confirmation_token\n5. publisher_job_get → Check job status (poll until completed)\n6. publisher_job_files → Download generated files\n```\n\n### Upload and Use Media\n\n```\n1. upload_create → Get presigned upload URL\n2. [Upload file to presigned URL]\n3. upload_process → Process the uploaded file\n4. upload_get → Check its status (poll until success or failed)\n5. media_list → Verify media is available\n6. template_update → Reference media in template\n```\n\n### Set Up a Webhook\n\n```\n1. webhook_list_event_types → Choose the events to receive\n2. webhook_create → Ask the user to confirm (confirmation_required), then create\n   the subscription with the confirmation_token; store the secret it returns\n3. webhook_list_deliveries → Check that deliveries reach the endpoint\n4. webhook_retry_delivery → Send a failed delivery again once the endpoint is fixed\n```\n\n### Share a Project\n\n```\n1. project_get → Get project details\n2. invitation_create → Ask the user to confirm (confirmation_required), then\n   invite the collaborators (emails) with the confirmation_token\n3. role_list → Verify roles assigned\n```","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#security","section":"Security","audience":"api","headings":["No stored tokens","Output Sanitization"],"text":"### No stored tokens\n\nThe MCP server is an OAuth 2.0 resource server: it passes each request's token to the Typograph API and stores none. There are no sessions to take over.\n\n### Output Sanitization\n\nSensitive data (tokens, secrets) is automatically redacted from tool responses. The one exception is a webhook's signing secret, which `webhook_create` and `webhook_rotate_secret` return on purpose (see [Webhooks](https://docs.typograph.nl/guides/api/mcp-server#webhooks)): it only signs the deliveries to your own endpoint, cannot call the API, and `webhook_rotate_secret` with `revoke_previous` withdraws it at once (a plain rotate keeps the previous secret valid for 24 hours).","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#configuration","section":"Configuration","audience":"api","headings":["Environment Variables"],"text":"### Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `TYPOGRAPH_API_BASE_URL` | API base URL the tools call | `https://api.typograph.nl` |\n| `MCP_SERVER_PUBLIC_URL` | The server's own URL, in the protected resource metadata and the 401 | `https://mcp.typograph.nl` |\n| `OAUTH_ISSUER_URL` | The authorization server the metadata points to | `https://api.typograph.nl` |\n| `SERVICE_DOCUMENTATION_URL` | Documentation the metadata links to | `https://docs.typograph.nl` |\n| `MCP_SERVER_HOST` | Server host | `0.0.0.0` |\n| `MCP_SERVER_PORT` | Server port | `3000` |\n| `DOCS_INDEX_URL` | The `docs-index.json` that `docs_search` and `docs_get` read | `https://docs.typograph.nl/docs-index.json` |","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#rate-limits","section":"Rate Limits","audience":"api","headings":[],"text":"MCP server requests ultimately hit the Typograph API Gateway, so they're subject to the same tiered rate limits — categorized per endpoint and driven by your organization's subscription. See [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting).","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#best-practices","section":"Best Practices","audience":"api","headings":["For AI Agent Developers","Error Handling"],"text":"### For AI Agent Developers\n\n1. **Handle a 401** - Sign the user in again when the server answers `401 Unauthorized`; MCP clients that support OAuth do this themselves\n2. **Poll job status** - Use exponential backoff when checking job completion\n3. **Use pagination** - Large result sets are paginated; iterate through pages. A list tool's `page_size` is at most 50, as the API answers; `organization_member_list`, `organization_invitation_list`, `organization_invitation_list_mine` and `premade_template_list` take up to 100. A larger `page_size` is refused with `validation_error`\n4. **Cache team/project IDs** - Reduce API calls by caching resource identifiers\n5. **Check scopes** - Verify your client has required scopes before operations\n6. **Ask the user on `confirmation_required`** - Show the summary and the steps, and repeat the call with the `confirmation_token` only once the user agreed ([confirmation](https://docs.typograph.nl/guides/api/mcp-server#tools-that-ask-for-confirmation))\n\n### Error Handling\n\n```javascript\nasync function callTool(name, args) {\n  // An MCP client that supports OAuth signs in again by itself on a 401.\n  const response = await mcpClient.callTool(name, args);\n  const result = JSON.parse(response.content[0].text);\n\n  if (result.status === 'confirmation_required') {\n    // Nothing happened yet: ask the user, and call again only when they agree.\n    const isAgreed = await askUser(result.summary, result.steps);\n    return isAgreed\n      ? callTool(name, { ...args, confirmation_token: result.confirmation_token })\n      : undefined;\n  }\n\n  if (!result.success) {\n    console.error(`Error: ${result.error.code} - ${result.error.message}`);\n    return;\n  }\n\n  return result.data;\n}\n```","agent_tools":[]},{"title":"AI Agents","url":"https://docs.typograph.nl/guides/api/mcp-server#standards-compliance","section":"Standards Compliance","audience":"api","headings":[],"text":"- **Transport**: Streamable HTTP (introduced in protocol revision 2025-03-26) — a single `POST /mcp` endpoint answering with plain JSON, no SSE stream. Clients that only support the older HTTP+SSE transport (`GET /sse`) are not supported.\n- **MCP Protocol**: `initialize` reports `protocolVersion: 2024-11-05`, although the transport is the newer one above. Claude Code connects with this pairing; other clients follow the same negotiation but are not all verified.\n- **OAuth 2.0**: RFC 6749, RFC 7636 (PKCE)\n- **OAuth Metadata**: RFC 8414, RFC 9728\n- **JSON-RPC**: 2.0 specification; a batch holds at most 20 requests","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview","section":null,"audience":"api","headings":[],"text":"The Typograph API lets your systems, and AI agents acting for your users, manage templates and projects, generate press-ready documents, convert files, and collaborate. For AI agents, the [MCP server](https://docs.typograph.nl/guides/api/mcp-server) offers the same capabilities as tools.\n\nAn integration built on names from before Typograph's naming convention: [Renamed fields and paths](https://docs.typograph.nl/guides/api/renamed-names) lists every old name and its new one.","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#base-url","section":"Base URL","audience":"api","headings":[],"text":"```\nhttps://api.typograph.nl\n```","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#api-gateway-architecture","section":"API Gateway Architecture","audience":"api","headings":[],"text":"All API requests go through the Typograph API Gateway, which handles:\n\n- **Authentication** - OAuth 2.0 token validation\n- **Authorization** - Scope-based access control\n- **Rate Limiting** - Request throttling per client\n- **Request Routing** - Proxying to internal microservices","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#services","section":"Services","audience":"api","headings":[],"text":"The API consists of these services:\n\n| Service | Description | Base Path | Scope |\n|---------|-------------|-----------|-------|\n| **Identity** | Users, sessions, organizations, OAuth clients | `/v1/identity/` | `identity` |\n| **File** | Teams, projects, templates, media, sharing | `/v1/file/` | `file` |\n| **Publisher** | PDF and image generation | `/v1/publisher/` | `publisher` |\n| **Converter** | File format conversion | `/v1/converter/` | `converter` |\n| **Document** | Template validation and schema upgrade | `/v1/document/` | `document` |\n| **Webhook** | Event subscriptions and deliveries | `/v1/webhook/` | `webhook` |\n| **Subscription** | Subscription plan, usage, add-ons | `/v1/subscription/` | `subscription` |","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#quick-start","section":"Quick Start","audience":"api","headings":["1. Get an Access Token","2. Make API Requests"],"text":"### 1. Get an Access Token\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=file document publisher\"\n```\n\n### 2. Make API Requests\n\n```bash\ncurl https://api.typograph.nl/v1/file/teams \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#rate-limiting","section":"Rate Limiting","audience":"api","headings":["Rate Limit Headers","Rate Limit Exceeded"],"text":"Rate limits are **tiered by your organization's subscription** and split across four layers (burst, hourly, daily, monthly) and five endpoint categories (`general`, `file`, `converter`, `publisher`, `webhook`), plus `ai` for the portal's AI assistant. Anonymous requests are capped at 100/hour on the `general` category only.\n\n### Rate Limit Headers\n\nResponses use the [IETF draft rate-limit headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/):\n\n| Header | Description |\n|--------|-------------|\n| `RateLimit` | Current status, format: `\"category\";r=remaining;t=reset_seconds` |\n| `RateLimit-Policy` | Configured policies (quota + window per layer) |\n| `Retry-After` | On 429, seconds until you should retry |\n\nExample:\n\n```\nRateLimit: \"general\";r=3542;t=1800\nRateLimit-Policy: \"general_hourly\";q=3600;w=3600, \"general_burst\";q=50;w=60\n```\n\n### Rate Limit Exceeded\n\n```json\n{\n  \"error\": \"rate_limit_exceeded\",\n  \"error_description\": \"Hourly rate limit exceeded for general endpoints.\"\n}\n```\n\nSee [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting) for the full breakdown, per-layer error codes, and handler patterns.","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#request-headers","section":"Request Headers","audience":"api","headings":["Required Headers","Response Headers"],"text":"### Required Headers\n\n| Header | Description |\n|--------|-------------|\n| `Authorization` | `Bearer {access_token}` for authenticated requests |\n| `Content-Type` | `application/json` for request bodies |\n\n### Response Headers\n\n| Header | Description |\n|--------|-------------|\n| `X-Powered-By` | `Typograph (www.typograph.nl)` |\n| `X-Typograph-Request-Id` | UUID v7 correlation ID for support |\n| `X-Typograph-User-Id` | The ID of the person your token acts for; absent for a token without a person (client credentials) |\n| `X-Typograph-Organization-Id` | The ID of the organization the request worked in, the one your token reaches ([Which organization a token reaches](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)) |\n| `RateLimit` | Current window status (see [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting)) |\n| `RateLimit-Policy` | Configured rate-limit policies |","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#asynchronous-processing","section":"Asynchronous Processing","audience":"api","headings":["Webhook Notifications","File Retention"],"text":"Most operations are processed asynchronously using a job-based architecture:\n\n```\n┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐\n│  Create Job │ ──▶ │  Processing │ ──▶ │  Completed  │ ──▶ │  Download   │\n│   (POST)    │     │   (async)   │     │   (poll)    │     │   (GET)     │\n└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘\n```\n\n1. **Create Job** - Submit a request, receive a job ID with `queued` status\n2. **Processing** - Job progresses through stages (downloading, processing, uploading)\n3. **Completion** - Job reaches `completed` or `failed` status\n4. **Download** - Retrieve generated files using the job ID\n\n### Webhook Notifications\n\nInstead of polling, subscribe to webhooks to receive real-time notifications:\n\n| Event | Description |\n|-------|-------------|\n| `publication.completed` | Publisher job finished successfully |\n| `publication.failed` | Publisher job failed |\n| `conversion.completed` | Converter job finished successfully |\n| `conversion.failed` | Converter job failed |\n\nSee [Webhooks](https://docs.typograph.nl/guides/api/webhooks) for setup, delivery semantics, and signature verification.\n\n### File Retention\n\nGenerated files are available for **4 days** after job completion. After this period, files expire and return `410 Gone`.","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#job-status-values","section":"Job Status Values","audience":"api","headings":["Publisher Job States","Converter Job States"],"text":"### Publisher Job States\n\n| Status | Description |\n|--------|-------------|\n| `queued` | Job is waiting to be processed |\n| `queued_by_csv_processor` | Child job queued by CSV batch processing |\n| `queued_by_image_generator` | Job queued for image generation |\n| `downloading` | Downloading template and data files |\n| `processing_csv` | Processing CSV data file |\n| `waiting_for_rows` | A CSV merge whose rows are queued, waiting for them to print; `printed_row_count` and `failed_row_count` show how far it is |\n| `merging` | A CSV merge whose rows have all ended, being finished |\n| `generating` | Generating output document |\n| `uploading` | Uploading generated files |\n| `completed` | Job finished successfully |\n| `failed` | Job encountered an error |\n| `expired` | Generated files have expired |\n\n### Converter Job States\n\n| Status | Description |\n|--------|-------------|\n| `queued` | Job is waiting to be processed |\n| `downloading` | Downloading source file |\n| `converting` | Converting file to target format |\n| `uploading` | Uploading converted file |\n| `completed` | Conversion finished successfully |\n| `failed` | Conversion encountered an error |\n| `expired` | Converted files have expired |","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#error-handling","section":"Error Handling","audience":"api","headings":["Error Response Format","Standard Error Codes","Validation Errors","Request Correlation"],"text":"The API uses standard HTTP status codes:\n\n| Code | Description |\n|------|-------------|\n| `200` | Success |\n| `201` | Resource created |\n| `204` | Success with no content |\n| `400` | Invalid request parameters |\n| `401` | Authentication required |\n| `403` | Insufficient permissions (scope) |\n| `404` | Resource not found |\n| `410` | File expired (no longer available) |\n| `422` | Validation failure |\n| `429` | Rate limit exceeded |\n| `500` | Internal server error |\n| `503` | Service unavailable |\n\n### Error Response Format\n\nError responses follow a hybrid format compatible with OAuth 2.0 (RFC 6749) and inspired by RFC 9457 Problem Details:\n\n```json\n{\n  \"error\": \"invalid_request\",\n  \"error_description\": \"Email is required.\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\"\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `error` | string | Machine-readable error code |\n| `error_description` | string | Human-readable error message |\n| `request_id` | string | UUID v7 correlation ID (also in `X-Typograph-Request-Id` header) |\n| `type` | string | *(optional)* URI to error documentation |\n| `details` | array | *(optional)* Field-level validation errors |\n\n### Standard Error Codes\n\n| HTTP Status | Error Code | Description |\n|-------------|------------|-------------|\n| `400` | `invalid_request` | Malformed request or missing parameters |\n| `400` | `validation_error` | Field validation failures |\n| `401` | `unauthorized` | Missing or invalid authentication |\n| `401` | `invalid_token` | Token expired or revoked |\n| `403` | `access_denied` | Insufficient permissions or scope |\n| `404` | `not_found` | Resource does not exist |\n| `405` | `method_not_allowed` | HTTP method not allowed |\n| `409` | `conflict` | Resource conflict (duplicate, concurrent edit) |\n| `409` | `in_trash` | What the call works on is in the trash, or in a project or team that is: restore it first (see [File Service](https://docs.typograph.nl/guides/api/file-service#nothing-works-on-what-is-in-the-trash)) |\n| `409` | `container_in_trash` | A restore of something whose project or team is still in the trash |\n| `409` | `rotation_in_progress` | A webhook secret rotation inside the 24-hour overlap of the previous one (see [Rotate Secret](https://docs.typograph.nl/guides/api/webhooks#rotate-secret)) |\n| `410` | `gone` | The files of a publisher or converter job that has expired (see [File Retention](https://docs.typograph.nl/guides/api/overview#file-retention)) |\n| `422` | `validation_error` | Valid syntax but semantic validation failed |\n| `429` | `rate_limit_exceeded` | Too many requests |\n| `500` | `server_error` | Internal server error |\n| `503` | `service_unavailable` | Service temporarily unavailable |\n\nA few endpoints return a code of their own, listed with the endpoint. Using a premade template ([File Service](https://docs.typograph.nl/guides/api/file-service#premade-templates)) can answer `400` with `premade_template_not_ready` (no package yet), `premade_template_unavailable` (the package could not be downloaded) or `premade_template_invalid` (the package is not an importable zip).\n\n### Validation Errors\n\nWhen validation fails, the response includes a `details` array with field-level errors:\n\n```json\n{\n  \"error\": \"validation_error\",\n  \"error_description\": \"Request validation failed\",\n  \"request_id\": \"019397ab-cdef-7890-abcd-ef1234567890\",\n  \"details\": [\n    {\n      \"field\": \"email\",\n      \"code\": \"required\",\n      \"message\": \"Email is required.\"\n    },\n    {\n      \"field\": \"handle\",\n      \"code\": \"too_long\",\n      \"message\": \"Handle must be at most 255 characters.\"\n    }\n  ]\n}\n```\n\n### Request Correlation\n\nEvery request is assigned a unique `request_id` (UUID v7):\n\n- Returned in the `X-Typograph-Request-Id` response header on all responses\n- Included in the JSON body for error responses\n- Used for request tracing and debugging\n\n**When [contacting support](https://docs.typograph.nl/guides/support)**, always include the `request_id` from the error response for faster issue resolution.","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#api-discovery","section":"API Discovery","audience":"api","headings":["OAuth Server Metadata","API Home Document"],"text":"### OAuth Server Metadata\n\nDiscover OAuth endpoints programmatically:\n\n```bash\ncurl https://api.typograph.nl/.well-known/oauth-authorization-server\n```\n\n### API Home Document\n\nGet API metadata and the gateway's routes, with their methods and scopes. It needs no token and counts against the anonymous rate limit:\n\n```bash\ncurl https://api.typograph.nl/\n```\n\nIt answers `name`, `version`, `website_url`, `portal_url` and `documentation_url`, the OAuth `endpoints`\nby name, and under `resources` the routes of each service, each with its `path`, `methods`, `scopes` and\n`authentication`: `public` (no token), `token` (an access token) or `internal` (for the platform's own\nservices).\n\nSupports JSON Home format ([RFC draft](https://datatracker.ietf.org/doc/html/draft-nottingham-json-home-06)):\n\n```bash\ncurl https://api.typograph.nl/ \\\n  -H \"Accept: application/json-home\"\n```","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#openapi-reference","section":"OpenAPI Reference","audience":"api","headings":[],"text":"Explore the complete API specification with the interactive Swagger UI:\n\n**[Open OpenAPI Reference →](https://docs.typograph.nl/api-reference)**","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#using-the-api","section":"Using the API","audience":"api","headings":[],"text":"Typograph's REST API speaks standard HTTP + JSON, so any HTTP client works — `fetch`/`axios` in the browser or Node.js, `requests` in Python, `cURL`, Postman, etc. See [Authentication](https://docs.typograph.nl/guides/api/authentication) for how to obtain an access token, and browse the [OpenAPI Reference](https://docs.typograph.nl/api-reference) for the full endpoint surface.","agent_tools":[]},{"title":"API Overview","url":"https://docs.typograph.nl/guides/api/overview#support","section":"Support","audience":"api","headings":[],"text":"- **Documentation**: [docs.typograph.nl](https://docs.typograph.nl)\n- **Contact**: the [support guide](https://docs.typograph.nl/guides/support)\n- **Status Page**: [status.typograph.nl](https://status.typograph.nl)","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination","section":null,"audience":"api","headings":[],"text":"All list endpoints in the Typograph API use **offset-based pagination** with a consistent response format.","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#query-parameters","section":"Query Parameters","audience":"api","headings":["Limit Constraints"],"text":"| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `limit` | integer | 10-20 | Number of items per page |\n| `offset` | integer | 0 | Number of items to skip |\n\n### Limit Constraints\n\n- **Minimum**: 1\n- **Maximum**: 50 for most endpoints; the few that take 100 are listed [below](https://docs.typograph.nl/guides/api/pagination#endpoints-supporting-pagination)\n- **Default**: Varies by endpoint (typically 10 or 20)\n\nExceeding the maximum limit returns a `400 Bad Request` error:\n\n```json\n{\n  \"error\": \"invalid_request\",\n  \"error_description\": \"The limit can't be greater than 50.\"\n}\n```","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#response-format","section":"Response Format","audience":"api","headings":["Response Fields"],"text":"All paginated endpoints return a standardized response structure:\n\n```json\n{\n  \"data\": [\n    { \"id\": \"...\", \"name\": \"...\" },\n    { \"id\": \"...\", \"name\": \"...\" }\n  ],\n  \"meta\": {\n    \"total\": 150,\n    \"limit\": 20,\n    \"offset\": 0,\n    \"has_more\": true\n  }\n}\n```\n\n`GET /v1/file/notifications` is the exception: it takes `limit` and `offset` the same way, but answers\n`{ \"results\": [...], \"total\": 12 }` without `meta`. A next page exists while `offset + limit` is\nbelow `total`.\n\n### Response Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `data` | array | Array of resource objects |\n| `meta.total` | integer | Total number of items available |\n| `meta.limit` | integer | Number of items requested per page |\n| `meta.offset` | integer | Number of items skipped |\n| `meta.has_more` | boolean | Whether more items exist beyond this page |","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#usage-examples","section":"Usage Examples","audience":"api","headings":["First Page","Next Page","Last Page"],"text":"### First Page\n\n```bash\ncurl \"https://api.typograph.nl/v1/file/projects?limit=20&offset=0\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nResponse:\n```json\n{\n  \"data\": [...],\n  \"meta\": {\n    \"total\": 150,\n    \"limit\": 20,\n    \"offset\": 0,\n    \"has_more\": true\n  }\n}\n```\n\n### Next Page\n\n```bash\ncurl \"https://api.typograph.nl/v1/file/projects?limit=20&offset=20\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nResponse:\n```json\n{\n  \"data\": [...],\n  \"meta\": {\n    \"total\": 150,\n    \"limit\": 20,\n    \"offset\": 20,\n    \"has_more\": true\n  }\n}\n```\n\n### Last Page\n\nWhen you reach the last page, `has_more` will be `false`:\n\n```json\n{\n  \"data\": [...],\n  \"meta\": {\n    \"total\": 150,\n    \"limit\": 20,\n    \"offset\": 140,\n    \"has_more\": false\n  }\n}\n```","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#iterating-through-all-results","section":"Iterating Through All Results","audience":"api","headings":["JavaScript/TypeScript","PHP"],"text":"### JavaScript/TypeScript\n\n```typescript\nasync function getAllItems(endpoint: string, token: string) {\n  const items = [];\n  let offset = 0;\n  const limit = 50; // The maximum of most endpoints\n\n  while (true) {\n    const response = await fetch(\n      `${endpoint}?limit=${limit}&offset=${offset}`,\n      { headers: { Authorization: `Bearer ${token}` } }\n    );\n    const { data, meta } = await response.json();\n\n    items.push(...data);\n\n    if (!meta.has_more) break;\n    offset += limit;\n  }\n\n  return items;\n}\n```\n\n### PHP\n\n```php\nfunction getAllItems(string $endpoint, string $token): array\n{\n    $items = [];\n    $offset = 0;\n    $limit = 50; // The maximum of most endpoints\n\n    do {\n        $response = $client->request('GET', \"{$endpoint}?limit={$limit}&offset={$offset}\", [\n            'headers' => ['Authorization' => \"Bearer {$token}\"],\n        ]);\n\n        $result = json_decode($response->getContent(), true);\n        $items = array_merge($items, $result['data']);\n        $hasMore = $result['meta']['has_more'];\n        $offset += $limit;\n    } while ($hasMore);\n\n    return $items;\n}\n```","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#best-practices","section":"Best Practices","audience":"api","headings":["Choose an Appropriate Page Size","Handle Edge Cases","Efficient Pagination"],"text":"### Choose an Appropriate Page Size\n\n| Use Case | Recommended Limit |\n|----------|-------------------|\n| Initial page load | 10-20 |\n| Infinite scroll | 20-30 |\n| Data export/sync | The endpoint's maximum: 50, or 100 where the [table below](https://docs.typograph.nl/guides/api/pagination#endpoints-supporting-pagination) says so |\n| Search results | 10-20 |\n\n### Handle Edge Cases\n\n1. **Empty Results**: When `total` is 0, `data` will be an empty array\n2. **Invalid Offset**: If offset exceeds total, returns empty `data` with `has_more: false`\n3. **Concurrent Updates**: Items may shift between pages if data changes during pagination\n\n### Efficient Pagination\n\n- **Cache Total Count**: The `total` value can help display page numbers or progress indicators\n- **Use Maximum Limit for Exports**: When syncing all data, use the endpoint's maximum `limit` (50, or 100 for the endpoints the [table below](https://docs.typograph.nl/guides/api/pagination#endpoints-supporting-pagination) lists) to minimize requests\n- **Check `has_more`**: More reliable than calculating from offset + length vs total","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#endpoints-supporting-pagination","section":"Endpoints Supporting Pagination","audience":"api","headings":[],"text":"| Endpoint | Default Limit | Max Limit |\n|----------|---------------|-----------|\n| `GET /v1/file/projects` | 10 | 50 |\n| `GET /v1/file/templates` | 10 | 50 |\n| `GET /v1/file/projects/{projectId}/uploads` | 10 | 50 |\n| `GET /v1/file/teams` | 10 | 50 |\n| `GET /v1/file/notifications` | 10 | 50 |\n| `GET /v1/publisher/jobs` | 10 | 50 |\n| `GET /v1/converter/jobs` | 10 | 50 |\n| `GET /v1/webhook/subscriptions` | 20 | 50 |\n| `GET /v1/webhook/subscriptions/{id}/deliveries` | 20 | 50 |\n| `GET /v1/identity/oauth/clients` | 10 | 50 |\n| `GET /v1/identity/organizations/{organizationId}/members` | 50 | 100 |\n| `GET /v1/identity/organizations/{organizationId}/invitations` | 50 | 100 |\n| `GET /v1/identity/users/me/invitations` | 50 | 100 |\n| `GET /v1/file/premade-templates` | 50 | 100 |","agent_tools":[]},{"title":"Pagination","url":"https://docs.typograph.nl/guides/api/pagination#related","section":"Related","audience":"api","headings":[],"text":"- [API Overview](https://docs.typograph.nl/guides/api/overview) - General API concepts\n- [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting) - Request limits and throttling","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options","section":null,"audience":"api","headings":[],"text":"A publisher job's look on paper is set in `output.config`. This page lists every option, what it\naccepts and what it does. Everything is optional: leave an option out and you get the default.\n\n```json\n{\n  \"output\": {\n    \"upload\": { \"url\": \"https://example.com/upload\" },\n    \"config\": {\n      \"type\": \"pdf\",\n      \"color_mode\": \"cmyk\",\n      \"dpi\": 300,\n      \"bleed\": 3,\n      \"pages\": \"1-2, 5\",\n      \"crop_marks\": true,\n      \"single_pages\": true,\n      \"rotate_pages\": [{ \"page\": 2, \"degrees\": 90 }],\n      \"add_blank_last_page\": true\n    }\n  }\n}\n```\n\nAn invalid value is refused when the job is created, with a message naming the option — the job\nis not started.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#overview","section":"Overview","audience":"api","headings":[],"text":"| Option | Type | Default | Accepts |\n|---|---|---|---|\n| `type` | string | `pdf` | `pdf`, `png`, `jpg`, `jpeg`, `webp` |\n| `color_mode` | string | `rgb` | `rgb`, or for `pdf`, `jpg` and `jpeg`: `cmyk`, or `cmyk+spots` to print each spot swatch as a separation of its own (its CMYK is the alternate) |\n| `dpi` | integer | `300` | 36 to 600 |\n| `quality` | integer | `90` | 0 to 100 — JPEG only |\n| `bleed` | number | the document's own | millimetres, 0 or more |\n| `pages` | string | every page | page numbers and ranges — [syntax below](https://docs.typograph.nl/guides/api/pdf-options#pages) |\n| `crop_marks` | boolean | `false` | |\n| `single_pages` | boolean | `false` | |\n| `rotate_pages` | list | none | `{page, degrees}` pairs — [syntax below](https://docs.typograph.nl/guides/api/pdf-options#rotate_pages) |\n| `add_blank_last_page` | boolean | `false` | |\n| `watermark` | object | none | [see below](https://docs.typograph.nl/guides/api/pdf-options#watermark) |","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#how-pages-are-numbered","section":"How pages are numbered","audience":"api","headings":[],"text":"`pages` and `rotate_pages` use the same numbering: the order in which the PDF puts out its pages,\nstarting at 1.\n\n- **Without `single_pages`**, every spread is one page of the PDF — a two-page spread\n  counts as one.\n- **With `single_pages`**, every page is a page of the PDF — a two-page spread counts\n  as two.\n- **A cover wrap counts too.** The editor's page bar leaves a cover wrap out of its page numbers;\n  the PDF does not. In a document of four pages followed by a cover, the cover is page 5 here.\n\nNumbers refer to the whole document, before any selection: with `\"pages\": \"8-11\"`, page 9 in\n`rotate_pages` is still that page 9.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#pages","section":"pages","audience":"api","headings":[],"text":"Which pages to put out. Leave it out, or empty, for every page.\n\n| Write | Means |\n|---|---|\n| `\"3\"` | page 3 |\n| `\"2-5\"` | pages 2, 3, 4 and 5 |\n| `\"1-2, 8-11, 40\"` | pages 1, 2, 8, 9, 10, 11 and 40 |\n\nThe rules — the service checks them exactly:\n\n- Parts are separated by **a comma followed by one space**: `\"1, 3\"`. Without the space\n  (`\"1,3\"`), with only spaces (`\"1 3\"`), or with a space before the comma, the job is refused.\n- A range is two numbers joined by a hyphen, **without spaces**: `\"2-5\"`, not `\"2 - 5\"`.\n- Numbers start at 1, without leading zeros: `\"0\"` and `\"07\"` are refused.\n- The PDF keeps the document's order whatever order you write: `\"5, 2\"` puts out page 2, then 5.\n- A number past the last page selects nothing; it is not an error.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#crop_marks","section":"crop_marks","audience":"api","headings":[],"text":"Adds crop marks outside the page, for cutting it to its final size, and fold marks where two pages\nof a spread meet. Use it with a `bleed`, so the marks sit in the bleed area.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#single_pages","section":"single_pages","audience":"api","headings":[],"text":"Puts out each page of a spread as a page of its own. A two-page spread becomes two PDF pages, each\nwith bleed on its three outer edges only — never on the fold.\n\nThe publisher still reads `spreads_as_single_pages`, the option's name before, as a deprecated\nalias; send `single_pages`. When a request has both, `single_pages` wins.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#rotate_pages","section":"rotate_pages","audience":"api","headings":[],"text":"Turns chosen pages of the PDF. The page is turned whole — its content, bleed and crop marks\ntogether — and the document itself is not changed.\n\n```json\n\"rotate_pages\": [\n  { \"page\": 3, \"degrees\": 90 },\n  { \"page\": 5, \"degrees\": 180 }\n]\n```\n\n- `page`: a page number from 1, numbered as described [above](https://docs.typograph.nl/guides/api/pdf-options#how-pages-are-numbered).\n- `degrees`: `90`, `180` or `270`, clockwise.\n- Each page may appear once.\n- A page that is not put out (because of `pages`) is simply not there to turn.\n\nThe job is refused for a `page` below 1, any other number of degrees, or a page given twice.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#add_blank_last_page","section":"add_blank_last_page","audience":"api","headings":[],"text":"`true` adds one empty page after the last page — for a printer that needs an even page count, or a\nblank back.\n\n- It has the size of the last page; when the last page is a spread, the size of one page of it.\n- It gets the same bleed and crop marks as the page before it.\n- Nothing is printed on it: no content, no page colour, no watermark. It is never rotated.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#bleed","section":"bleed","audience":"api","headings":[],"text":"The bleed in millimetres, on every edge of every page, overriding what the document says. Leave it\nout to use each page's own bleed from the document.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#dpi-and-quality","section":"dpi and quality","audience":"api","headings":[],"text":"`dpi` is the resolution images are rendered at (36 to 600). `quality` is JPEG compression, 0 to\n100, and only applies to `jpg` and `jpeg` output.","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#watermark","section":"watermark","audience":"api","headings":[],"text":"An image laid over every page.\n\n```json\n\"watermark\": {\n  \"type\": \"center\",\n  \"image\": { \"url\": \"https://example.com/watermark.png\" },\n  \"width\": 50,\n  \"height\": 20,\n  \"opacity\": 0.3\n}\n```\n\n| Property | Accepts |\n|---|---|\n| `type` | `center` (once, in the middle) or `repeat_horizontal` (repeated across the page) |\n| `image.url` | the image; `image.headers` may carry up to 5 headers for fetching it |\n| `width`, `height` | whole millimetres, 1 or more; optional |\n| `opacity` | 0 to 1; default 0.5 |\n\nA CSV job on the Free plan prints with the \"PROOF · typograph.nl\" watermark instead of this one\n([How many rows your plan merges](https://docs.typograph.nl/guides/api/publisher#how-many-rows-your-plan-merges)).","agent_tools":[]},{"title":"PDF Output Options","url":"https://docs.typograph.nl/guides/api/pdf-options#availability","section":"Availability","audience":"api","headings":[],"text":"`rotate_pages` and `add_blank_last_page` are new in release 20261007 and work once the publisher\nservice of that release is deployed. A service that predates them refuses a job that uses them;\nleave them out and the job runs as before.\n\n`color_mode: cmyk+spots` is new in release 20261021 and works once the publisher service of that\nrelease is deployed. A service that predates it refuses the job; `cmyk` runs on every release,\nprinting spot swatches as their process equivalent.\n\nSee also: [Publisher Service](https://docs.typograph.nl/guides/api/publisher) for creating jobs, CSV batches and downloads.","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher","section":null,"audience":"api","headings":[],"text":"The Publisher service generates PDFs and images from Typograph templates. It supports batch processing with CSV data for generating multiple documents from a single template.\n\n**Base URL:** `/v1/publisher/`\n\n**Scopes:** `publisher` (full), `publisher:read`, `publisher:write`\n\n**Token type:** `any` — publisher endpoints accept either a user token or a client token. See [Token Types](https://docs.typograph.nl/guides/api/token-types).","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#endpoints","section":"Endpoints","audience":"api","headings":[],"text":"| Method | Path | Scopes | Description |\n|--------|------|--------|-------------|\n| `GET` | `/v1/publisher/jobs` | `publisher` / `publisher:read` | List generation jobs |\n| `POST` | `/v1/publisher/jobs` | `publisher` / `publisher:write` | Create a generation job |\n| `GET` | `/v1/publisher/jobs/{id}` | `publisher` / `publisher:read` | Job detail |\n| `GET` | `/v1/publisher/jobs/{id}/files` | `publisher` / `publisher:read` | Download the generated files, see [File Types](https://docs.typograph.nl/guides/api/publisher#file-types) |\n| `POST` | `/v1/publisher/merges/validate` | `publisher` / `publisher:write` | Check a merge before printing it, see [Checking a merge before printing](https://docs.typograph.nl/guides/api/publisher#checking-a-merge-before-printing) |\n| `GET` | `/health/publisher` | — | Service health (public, unauthenticated) |","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#create-a-generation-job","section":"Create a Generation Job","audience":"api","headings":["Response"],"text":"Generate a document from a Typograph template:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"invoice-alpha\",\n    \"input\": {\n      \"template\": {\n        \"url\": \"https://example.com/templates/invoice.json\"\n      },\n      \"manifest\": {\n        \"url\": \"https://example.com/templates/manifest.json\"\n      }\n    },\n    \"output\": {\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      },\n      \"config\": {\n        \"type\": \"pdf\",\n        \"color_mode\": \"cmyk\",\n        \"bleed\": 3\n      }\n    }\n  }'\n```\n\n### Response\n\n```json\n{\n  \"id\": \"01234567-89ab-cdef-0123-456789abcdef\",\n  \"tag\": \"invoice-alpha\",\n  \"status\": \"queued\",\n  \"created_at\": \"<ISO 8601 timestamp>\"\n}\n```","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#request-structure","section":"Request Structure","audience":"api","headings":["Input Object","Fonts","Output Object"],"text":"| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `tag` | string | No | Optional tag to identify or group jobs |\n| `input` | object | Yes | The files to publish, see [Input Object](https://docs.typograph.nl/guides/api/publisher#input-object) |\n| `output` | object | Yes | Where and how to publish, see [Output Object](https://docs.typograph.nl/guides/api/publisher#output-object) |\n| `url_headers` | array | No | Header rules for the job's URLs, at most 10: see [Headers for the URLs of a job](https://docs.typograph.nl/guides/api/url-headers) |\n\n### Input Object\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `template` | object | Yes, unless `bundle` is given | Typograph template JSON |\n| `template.url` | string | Yes | URL to template file |\n| `template.headers` | object | No | HTTP headers for downloading |\n| `manifest` | object | With `template` only | Typograph manifest JSON |\n| `manifest.url` | string | Yes | URL to manifest file |\n| `manifest.headers` | object | No | HTTP headers for downloading |\n| `bundle` | object | Yes, unless `template` is given | Typograph template bundle JSON: template and manifest in one document |\n| `bundle.url` | string | Yes | URL to bundle file |\n| `bundle.headers` | object | No | HTTP headers for downloading |\n| `csv` | object | No | CSV data file for batch processing |\n| `csv.url` | string | No | URL to CSV file |\n| `csv.headers` | object | No | HTTP headers for downloading |\n| `mapping` | object | No | With a `csv`: which column fills which field, column => field key. See [Choosing columns and rows](https://docs.typograph.nl/guides/api/publisher#choosing-columns-and-rows) |\n| `rows` | string | No | With a `csv`: the rows to print, `\"3\"` or `\"1-10\"`, counted from 1 without the header |\n\n:::note Template and manifest, or a bundle\nGive **either** a `template` with its `manifest`, **or** a `bundle` — never both, and no `manifest` next to a `bundle`: the bundle carries its own.\n\n- **`bundle`** is the `template_bundle.json` the [Converter Service](https://docs.typograph.nl/guides/api/converter#typograph_package-output) produces (or the `templatebundle.json` of a package made before that name) — `{ \"version\": \"1.0.0\", \"template\": {...}, \"manifest\": {...} }` — so a converted package can be published from one URL.\n- **`template` + `manifest`** suits a manifest whose image and font URLs expire, such as presigned storage URLs: keep one template and send a freshly generated manifest with every job.\n\nEither way, the per-URL `headers` apply to the bundle, template and manifest files themselves, not to the image and font URLs the manifest names. When those assets need a header, add a [`url_headers`](https://docs.typograph.nl/guides/api/url-headers) rule that covers them.\n:::\n\n### Fonts\n\nEvery font the template's text is set in must be in the manifest, with its files. When text is set in a font or style the job does not have, the job fails instead of printing without that text, and its `message` names the text and the font:\n\n```text\nThe text \"Hello\" is set in \"Lato\" (bold-italic), which is not among the job's fonts, so it cannot be printed.\n```\n\nAdd the font's files to the manifest, or set the text in a font the manifest has, then submit the job again. A space or other invisible character in a missing font does not fail the job.\n\n### Output Object\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `upload` | object | Yes | Target for generated files |\n| `upload.url` | string | Yes | URL to upload generated files |\n| `upload.headers` | object | No | HTTP headers for uploading |\n| `config` | object | Yes | Generator configuration |\n| `merge` | object | No | With a `csv`: how the merge delivers its rows. See [One file for the merge](https://docs.typograph.nl/guides/api/publisher#one-file-for-the-merge) |\n| `merge.mode` | string | No | `single` (one PDF, the default for a PDF) or `per_row` (a ZIP with a file per row, always for images) |\n| `merge.filename` | string | No | With `per_row`: each row's file name, with field keys in `{{…}}`. `file_name`, its name before, is still read as a deprecated alias |\n| `merge.on_overflow` | string | No | `fail` (the default) or `warn`: whether a row whose text does not fit fails or prints cut off |","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#output-formats","section":"Output Formats","audience":"api","headings":[],"text":"| Format | Description |\n|--------|-------------|\n| `pdf` | PDF document |\n| `png` | PNG image (one per page) |\n| `jpg` / `jpeg` | JPEG image (one per page) |\n| `webp` | WebP image (one per page) |\n\nAn image job of more than one page makes a **child job** per page, each with the parent's id as\nits `parent_id`. The parent writes no image of its own: it completes once its pages are queued,\nuploads nothing, and downloading its output answers `400`. Each page's image is fetched from its child job (see\n[Listing Child Jobs](https://docs.typograph.nl/guides/api/publisher#listing-child-jobs) and [Download Generated Files](https://docs.typograph.nl/guides/api/publisher#download-generated-files)).\nA job with `pages` set to one page makes one image.","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#generator-configuration","section":"Generator Configuration","audience":"api","headings":["Watermark Configuration"],"text":"Configure the output with the `output.config` object:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `type` | string | Output format: `pdf`, `png`, `jpg`, `jpeg`, `webp` |\n| `dpi` | integer | Resolution for images (36-600, default: 300) |\n| `quality` | integer | Quality for lossy formats (0-100, default: 90) |\n| `color_mode` | string | Color space: `rgb`, `cmyk`, or `cmyk+spots` (spot swatches as separations of their own) |\n| `bleed` | number | Print bleed in millimeters, 0 or more |\n| `pages` | string | Pages to put out, e.g. `\"1-5, 8-10\"` — a comma **and a space** between parts |\n| `crop_marks` | boolean | Include crop marks for printing |\n| `single_pages` | boolean | Split spread pages. `spreads_as_single_pages`, its name before, is still read as a deprecated alias |\n| `rotate_pages` | list | Pages to turn, e.g. `[{\"page\": 2, \"degrees\": 90}]` — 90, 180 or 270 |\n| `add_blank_last_page` | boolean | Add one empty page after the last |\n| `watermark` | object | Watermark overlay configuration |\n\nThe exact syntax of every option — how pages are numbered, what `pages` accepts, the rules for\n`rotate_pages` — is on [PDF Output Options](https://docs.typograph.nl/guides/api/pdf-options).\n\n### Watermark Configuration\n\nAdd a watermark overlay to generated documents:\n\n```json\n{\n  \"output\": {\n    \"config\": {\n      \"type\": \"pdf\",\n      \"watermark\": {\n        \"type\": \"center\",\n        \"image\": {\n          \"url\": \"https://example.com/watermark.png\",\n          \"headers\": {\n            \"Authorization\": \"Bearer your-storage-token\"\n          }\n        },\n        \"width\": 50,\n        \"height\": 20,\n        \"opacity\": 0.3\n      }\n    }\n  }\n}\n```\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `type` | string | Positioning: `center` or `repeat_horizontal` |\n| `image` | object | Watermark image URL and optional headers |\n| `image.url` | string | URL to PNG watermark image |\n| `image.headers` | object | Optional HTTP headers for downloading (max 5) |\n| `width` | number | Width in millimeters |\n| `height` | number | Height in millimeters |\n| `opacity` | number | Transparency (0.0-1.0, default: 0.5) |\n\nA CSV job on the Free plan prints with the \"PROOF · typograph.nl\" watermark instead of this one: see [How many rows your plan merges](https://docs.typograph.nl/guides/api/publisher#how-many-rows-your-plan-merges).","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#text-laid-out-again-before-printing","section":"Text Laid Out Again Before Printing","audience":"api","headings":[],"text":"A template stores where each word of its text sits, as the editor laid it out. The publisher prints\nthat layout, and lays the text out again first when the stored layout no longer matches the job:\n\n- the job's manifest binds a font to another family than the template was laid out in, for example a\n  theme's font;\n- a frame's text has changed since it was laid out, for example by a form or an API caller that\n  wrote new values into the template.\n\nWhen text no longer fits its frame after that layout, for example a longer name or a wider font, the\njob fails instead of printing the text cut off. Its `message` names the frames, by name:\n\n```text\nThe template was laid out again, but its text does not fit in \"Name\", so it would print cut off.\n```\n\nMake the frame larger or shorten the text, then submit the job again.\n\nOnly text that was laid out again counts. With other fonts, that is every frame. With changed text\nalone, it is the frames whose text changed: a frame you did not change prints as the editor shows it,\nalso when its text does not fit there. A template printed with the layout it was saved with is not\nchecked.","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#batch-processing-with-csv","section":"Batch Processing with CSV","audience":"api","headings":["Following a merge","What the CSV holds","Choosing columns and rows","One file for the merge","Checking a merge before printing","How many rows your plan merges","Listing Child Jobs"],"text":"Generate multiple documents by providing a CSV data file:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"invoice-batch-alpha\",\n    \"input\": {\n      \"template\": {\n        \"url\": \"https://example.com/templates/invoice.json\"\n      },\n      \"manifest\": {\n        \"url\": \"https://example.com/templates/manifest.json\"\n      },\n      \"csv\": {\n        \"url\": \"https://example.com/data/invoices.csv\"\n      }\n    },\n    \"output\": {\n      \"upload\": {\n        \"url\": \"https://example.com/webhook/upload\"\n      },\n      \"config\": {\n        \"type\": \"pdf\"\n      }\n    }\n  }'\n```\n\nWhen a CSV file is provided:\n- A **parent job** is created for the batch\n- **Child jobs** are automatically created for each row in the CSV\n- Use `parent_id` to filter and list child jobs\n\n### Following a merge\n\nThe parent job is the merge. It ends once every row has, not once the rows are queued:\n\n- **`waiting_for_rows`** while the rows print. `row_count` is the number of rows the merge prints, and `printed_row_count` and `failed_row_count` count those that printed and those that did not, so you can show progress by reading the parent with `GET /v1/publisher/jobs/{id}`. The three are `null` on a job without a CSV.\n- **`merging`** once every row printed, while the rows are put together into one file and uploaded.\n- **`completed`** once that file is uploaded to `output.upload.url`: see [One file for the merge](https://docs.typograph.nl/guides/api/publisher#one-file-for-the-merge). It is also the parent's own output, downloaded with `GET /v1/publisher/jobs/{id}/files`, and each row's file stays its child job's.\n- **`failed`** once every row ended and at least one did not print. A print run with holes is worse than none, so the merge fails as a whole. Its `message` names the first rows and why, and its `report` lists them all.\n- **One webhook for the merge**: `publication.completed` or `publication.failed` for the parent, carrying the counts and the report. The rows send none of their own.\n\nThe `report` tells what the merge has to say about your data:\n\n```json\n{\n  \"warnings\": [{ \"column\": \"Telefoon\", \"reason\": \"no_field\" }],\n  \"rows\": [\n    { \"row\": 2, \"status\": \"failed\", \"message\": \"The value \\\"misschien\\\" of \\\"photo.visible\\\" is not yes or no.\" },\n    { \"row\": 5, \"status\": \"warned\", \"message\": \"The text does not fit in \\\"naam\\\", so it printed cut off.\" }\n  ]\n}\n```\n\n- **`warnings`**, from the moment the CSV is read: each column that fills no field of the template (`no_field`), so a mistyped header does not go unnoticed.\n- **`rows`**, once the merge ended: every row that did not simply print, numbered as in the CSV from 1 (the header is not a row), with the reason: `failed` for a row that did not print, `warned` for one that printed cut off with `on_overflow: \"warn\"`. A reason longer than 300 characters is cut off with `…`; `GET /v1/publisher/jobs?parent_id=…` lists the rows' own jobs with their whole `message`. A merge that printed every row has an empty list.\n\n### What the CSV holds\n\nThe first line is the header, and each column it names fills a [merge field](https://docs.typograph.nl/guides/api/file-service#merge-fields) of the template: a named text frame, a named image, `<name>.visible`, or a `{{key}}` placeholder. `GET /v1/file/templates/{id}/fields` lists them. A column matches a field by its key or its label, ignoring case.\n\n```csv\nnaam;photo;photo.visible;plaats\nJan de Vries;https://example.com/jan.jpg;ja;Utrecht\nPiet;;nee;\n```\n\n- **Save it as your spreadsheet does.** The delimiter is the one the header uses most of `,`, `;` or a tab, so a Dutch Excel export with `;` works as it is. UTF-8 (with or without a byte order mark), Excel's \"Unicode text\" (UTF-16) and Windows-1252 are read. A quoted cell may hold the delimiter, quotes and line breaks.\n- **An empty cell leaves the element as the template has it.** In a placeholder's column it removes the placeholder, and its line when nothing else is on it.\n- **A column that fills no field is ignored**, and named in the report's `warnings`. The job fails before any row is queued when no column fills anything, or when a `{{key}}` of the template has no column: it would print as it stands.\n- **A `.visible` column says whether the element prints.** It prints for `true`, `yes`, `ja`, `1`, `x`, `y` or `j`, and is hidden for `false`, `no`, `nee`, `0` or `n`, in any case.\n- **A row that cannot be filled gets no child job.** A `.visible` value outside those, or an image value that is not an http(s) URL, makes such a row; the other rows still print. The merge then fails once they ended, and its `report` lists the row and the reason.\n- **A picture that does not answer fails its row**, rather than print as a grey box: `The picture of \"photo\" does not answer at its URL, so it cannot be printed.` Its URL is asked with the job's `url_headers` when the row prints, and must be public, as every image of a template must.\n- **A frame whose text changed is laid out again**, and a row whose text then does not fit its frame fails rather than printing cut off, unless `output.merge.on_overflow` is `warn`: then it prints cut off, and the report lists it as `warned`.\n- **Your plan sets the number of rows**, below.\n\n### Choosing columns and rows\n\n```json\n\"input\": {\n  \"template\": { \"url\": \"https://example.com/templates/badge.json\" },\n  \"manifest\": { \"url\": \"https://example.com/templates/manifest.json\" },\n  \"csv\": { \"url\": \"https://example.com/data/people.csv\" },\n  \"mapping\": { \"Naam\": \"name\", \"Toon foto\": \"photo.visible\" },\n  \"rows\": \"1-10\"\n}\n```\n\n- **`mapping`** names the field a column fills when its header does not match it, column => field key, at most 500. A column it leaves out still matches by key or label. A mapping that names a column the CSV does not have, or a field the template does not have, fails the job before any row is queued.\n- **`rows`** prints one row (`\"3\"`) or a range (`\"1-10\"`), counted from 1 without the header. The report numbers the rows as the CSV does, so row 7 is row 7 in your spreadsheet. Only these rows count against your plan, so a Free plan can proof rows 1–10 of a larger file. A range past the end fails the job: `The data has no row 5: it has 2 rows.`\n\nBoth go with a `csv`: without one, the job is refused.\n\n### One file for the merge\n\nWhen every row printed, the merge uploads **one file** to `output.upload.url`, and `output.merge` says which:\n\n- **`mode: \"single\"`**, the default for a PDF: every row in one PDF, in row order, with the trim, bleed and crop boxes of each page kept. A merge over 500 rows or 150 MB uploads a ZIP of such PDFs instead, `rows-1-500.pdf`, `rows-501-1000.pdf`, ….\n- **`mode: \"per_row\"`**, and always for an image merge: a ZIP with a file per row. `filename` names each file from the row's values, such as `\"{{name}}\"` for `Jan de Vries.pdf`. The file brings its own extension; a name that occurs twice gets `-2`, the pages of a multi-page image `-p1`, `-p2`, and without `filename` a file is `row-1.pdf`, `row-2.pdf`, ….\n\n`single` for an image merge, and `filename` without `per_row`, are refused when the job is created. A ZIP is uploaded with `Content-Type: application/zip`.\n\n### Checking a merge before printing\n\n`POST /v1/publisher/merges/validate` takes the body of the job that would start the merge and answers at once, without printing anything:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/merges/validate \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"input\": { \"template\": {...}, \"manifest\": {...}, \"csv\": {...}, \"mapping\": { \"Naam\": \"name\" } }, \"check_overflow_rows\": 5 }'\n```\n\n```json\n{\n  \"is_valid\": false,\n  \"message\": \"1 of the 250 rows would not print: the report says why.\",\n  \"plan\": { \"tier\": \"pro\", \"max_rows\": 1000, \"is_proof\": false },\n  \"row_count\": 250,\n  \"unchecked_row_count\": 0,\n  \"fields\": [{ \"key\": \"name\", \"label\": \"name\", \"type\": \"text\", \"required\": false, \"max_length\": null, \"choices\": null, \"targets\": [...] }],\n  \"mapping\": [{ \"column\": \"Naam\", \"field\": \"name\" }, { \"column\": \"notes\", \"field\": null }],\n  \"report\": {\n    \"warnings\": [{ \"column\": \"notes\", \"reason\": \"no_field\" }],\n    \"rows\": [{ \"row\": 7, \"status\": \"failed\", \"message\": \"The value \\\"maybe\\\" of \\\"badge.visible\\\" is not yes or no.\" }]\n  }\n}\n```\n\n- **`is_valid`** means the check found nothing that would stop the merge. A merge the job would refuse whole (a file that cannot be downloaded, a mapping that does not fit, more rows than your plan merges) answers `is_valid: false` with the job's own `message`. `plan.is_proof` is true on a Free plan, whose merges print as a proof. A body without a `csv`, which is no merge to check, or one the job would refuse too, is a 422.\n- **Every row** has its values checked. The template is filled in and validated for up to 25 different sets of fields the rows fill.\n- **The pictures** the rows name are asked whether they answer, as the job asks them: each URL once, for up to 25 URLs and 15 seconds. A row whose picture does not answer would not print.\n- **Text that does not fit** needs a layout, which takes a second or so a row. `check_overflow_rows` (0 to 20, default 0) lays out that many rows, the first that pass the other checks, for up to 30 seconds. A row whose text does not fit would not print, or with `output.merge.on_overflow: \"warn\"` is in the report as `warned`, which leaves the merge `is_valid`. A layout that takes over 20 seconds, or that the layout service does not make, leaves its row unchecked rather than failed.\n- **`unchecked_row_count`** counts the rows one of these limits left checked in part: their filled-in template, a picture, or their layout among the first `check_overflow_rows`. The rows after those are not laid out and not counted, so with the default of 0 nothing is laid out and `unchecked_row_count` can still be 0. `message` says when rows went unchecked, so `is_valid: true` is not read as more than was checked.\n- The check takes `publisher:write`, as creating the job does, and counts as one request.\n\n### How many rows your plan merges\n\nThe plan of the organization your token works for decides how many rows one job may print. What counts is the rows the job prints: every row of the CSV, or only those of its `rows` range. The job fails before any row is queued when they are more.\n\n| Plan | Rows per job |\n|---|---|\n| Free | 10, as a proof: every page prints with a \"PROOF · typograph.nl\" watermark |\n| Pro | 1,000 |\n| Enterprise | 10,000, or what your contract says |\n\n- **A Free proof always carries the watermark.** It replaces any `watermark` in your `output.config`, so a proof is never mistaken for the print run. A single job without a CSV prints without it.\n- **Too many rows** fails the job with a message that names the limit, for example `Your plan merges a proof of at most 10 rows, and this data has more.`\n- **The plan could not be read**: the job fails with `The organization's plan could not be checked. Try again in a moment.` Nothing merges unchecked. Create the job again.\n\n### Listing Child Jobs\n\n```bash\ncurl \"https://api.typograph.nl/v1/publisher/jobs?parent_id=PARENT_JOB_ID\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#download-generated-files","section":"Download Generated Files","audience":"api","headings":["File Types"],"text":"Download the output once the job is completed:\n\n```bash\ncurl \"https://api.typograph.nl/v1/publisher/jobs/{id}/files?file_type=output\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -o document.pdf\n```\n\n### File Types\n\n| Type | Description |\n|------|-------------|\n| `output` | Generated document (default) |\n| `template` | Original template file |\n| `manifest` | Original manifest file |\n| `csv` | CSV data file (if used) |","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#upload-request-format","section":"Upload Request Format","audience":"api","headings":["Minimal receiver (Node.js / Express)","Minimal receiver (PHP)","Uploading Directly to Cloud Storage"],"text":"When a job completes successfully and you've configured `output.upload.url`, Typograph sends the generated file as a **single HTTP PUT request** to that URL. Your upload target must accept this request shape:\n\n| Property | Value |\n|----------|-------|\n| Method | `PUT` |\n| URL | Exactly the URL you provided in `output.upload.url` (no redirects are followed) |\n| `Content-Type` | Detected from the file — e.g. `application/pdf`, `image/png`, `image/jpeg`, `image/webp`, or `application/zip` for a merge's ZIP |\n| `User-Agent` | `Typograph/Util-Upload` |\n| Custom headers | Whatever you passed in `output.upload.headers`, or else the headers of the [`url_headers`](https://docs.typograph.nl/guides/api/url-headers) rule that covers the upload URL (merged in, can't override `Content-Type` or `User-Agent`) |\n| Body | Raw file bytes, streamed (no multipart / form-data wrapper) |\n| Connection timeout | 10 seconds |\n| Transfer timeout | 300 seconds |\n\nYour endpoint must respond with an HTTP status in the **200–299** range to be considered successful; anything else marks the job as failed.\n\n### Minimal receiver (Node.js / Express)\n\n```javascript\nconst express = require('express');\nconst fs = require('fs');\nconst app = express();\n\napp.put('/uploads/:filename',\n  // Use a raw body parser — the request body is the file itself.\n  express.raw({ type: '*/*', limit: '200mb' }),\n  (req, res) => {\n    const filename = req.params.filename;\n    fs.writeFileSync(`./uploads/${filename}`, req.body);\n    console.log(`Received ${req.headers['content-type']} (${req.body.length} bytes)`);\n    res.status(200).send();\n  },\n);\n```\n\n### Minimal receiver (PHP)\n\n```php\n<?php\n// PUT /uploads/{filename}\n$rawBody = file_get_contents('php://input');\n$filename = basename($_SERVER['REQUEST_URI']);\nfile_put_contents('/var/uploads/' . $filename, $rawBody);\nhttp_response_code(200);\n```\n\n### Uploading Directly to Cloud Storage\n\nYou can point `output.upload.url` straight at a presigned URL from S3, GCS, Azure Blob, Cloudflare R2, etc. — each provider accepts signed PUT requests with a raw body. Set `output.upload.headers` to whatever the presigner expects (e.g. specific `Content-Type`, `x-amz-*` metadata). Protected headers (`Content-Type`, `User-Agent`) can't be overridden, so make sure the presigned URL is signed against the content type Typograph will send (see the list above).","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#custom-headers","section":"Custom Headers","audience":"api","headings":[],"text":"For URLs that need a header, such as a private CDN, give the job [`url_headers`](https://docs.typograph.nl/guides/api/url-headers): one list of rules whose headers go with every request to a URL a rule covers, including the fonts and images the manifest names.\n\n```json\n{\n  \"url_headers\": [\n    {\n      \"match\": \"https://storage.example.com/private/\",\n      \"headers\": { \"Authorization\": \"Bearer your-storage-token\" }\n    }\n  ],\n  \"input\": {\n    \"template\": { \"url\": \"https://storage.example.com/private/template.json\" },\n    \"manifest\": { \"url\": \"https://storage.example.com/private/manifest.json\" }\n  },\n  \"output\": {\n    \"upload\": { \"url\": \"https://example.com/webhook/upload\" },\n    \"config\": { \"type\": \"pdf\" }\n  }\n}\n```\n\nThe `headers` of a single URL are the shorthand for a rule that matches exactly that URL. They win over every prefix rule, and still work as before:\n\n```json\n{\n  \"input\": {\n    \"template\": {\n      \"url\": \"https://example.com/private/template.json\",\n      \"headers\": {\n        \"Authorization\": \"Bearer your-storage-token\"\n      }\n    },\n    \"manifest\": {\n      \"url\": \"https://example.com/private/manifest.json\",\n      \"headers\": {\n        \"Authorization\": \"Bearer your-storage-token\"\n      }\n    }\n  },\n  \"output\": {\n    \"upload\": {\n      \"url\": \"https://example.com/webhook/upload\",\n      \"headers\": {\n        \"X-API-Key\": \"your-api-key\"\n      }\n    }\n  }\n}\n```","agent_tools":[]},{"title":"Publisher Service","url":"https://docs.typograph.nl/guides/api/publisher#complete-example","section":"Complete Example","audience":"api","headings":[],"text":"Generate a PDF invoice with watermark and custom headers:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"tag\": \"invoice-premium-001\",\n    \"input\": {\n      \"template\": {\n        \"url\": \"https://storage.example.com/templates/invoice.json\",\n        \"headers\": {\n          \"Authorization\": \"Bearer storage-token\"\n        }\n      },\n      \"manifest\": {\n        \"url\": \"https://storage.example.com/templates/manifest.json\",\n        \"headers\": {\n          \"Authorization\": \"Bearer storage-token\"\n        }\n      }\n    },\n    \"output\": {\n      \"upload\": {\n        \"url\": \"https://webhook.example.com/documents\",\n        \"headers\": {\n          \"X-Webhook-Secret\": \"secret-key\"\n        }\n      },\n      \"config\": {\n        \"type\": \"pdf\",\n        \"color_mode\": \"cmyk\",\n        \"bleed\": 3,\n        \"crop_marks\": true,\n        \"watermark\": {\n          \"type\": \"center\",\n          \"image\": {\n            \"url\": \"https://storage.example.com/draft-watermark.png\",\n            \"headers\": {\n              \"Authorization\": \"Bearer storage-token\"\n            }\n          },\n          \"width\": 100,\n          \"height\": 30,\n          \"opacity\": 0.2\n        }\n      }\n    }\n  }'\n```","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting","section":null,"audience":"api","headings":[],"text":"The Typograph API enforces rate limits to ensure fair usage and maintain service quality for all users. We use a **multi-layer rate limiting system** with tiered subscriptions and endpoint categories.","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#organization-based-rate-limiting","section":"Organization-Based Rate Limiting","audience":"api","headings":["How It Works","One Free Organization per Person"],"text":"Rate limits in the Typograph API are tied to **organizations**, not individual clients or users. When you make an API request, the rate limits applied are determined by your organization's subscription plan.\n\n### How It Works\n\n1. **Every OAuth client belongs to an organization** - When you create an OAuth client, it's associated with your organization\n2. **Organization subscription determines limits** - Your organization's plan (Free, Pro, Enterprise) determines the rate limits\n3. **All clients in an organization share quota** - Rate limits are pooled at the organization level\n4. **On the Free plan, converter and publisher work also counts per person** - see below\n5. **The portal is not the API** - the portal's own requests count toward none of these categories, with two exceptions: publisher and converter work started there counts, for the organization that owns the project or template (see [Plans & Billing](https://docs.typograph.nl/portal/organization/billing#which-organization-your-work-counts-for)), and the requests of its AI assistant count in the `ai` category\n\n### One Free Organization per Person\n\nEvery organization on the Free plan has limits of its own. So that they do not add up:\n\n- **You own one organization on the Free plan.** Creating another while you own a free one is refused with `409` and `free_organization_limit_reached` (`POST /v1/identity/organizations`). A second organization goes on a paid plan.\n- **Converter and publisher requests on the Free plan count per person as well**, across every free organization you work in, against the Free limits below. A token that acts for a person (one from signing in, or an MCP client) is counted for its organization and for that person; the first limit reached refuses the request, with the error codes below and a message that says it counts per person. Paid plans and the other categories count per organization only, and so does an app's client-credentials token, which acts for no person.","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#rate-limit-tiers","section":"Rate Limit Tiers","audience":"api","headings":["Anonymous Tier","Free","Pro","Enterprise"],"text":"Rate limits vary based on your subscription plan. Every plan carries limits for five endpoint categories — `general`, `file`, `converter`, `publisher`, and `webhook` — across four time windows. A sixth category, `ai`, counts the requests of the portal's AI assistant (see [Endpoint Categories](https://docs.typograph.nl/guides/api/rate-limiting#endpoint-categories)).\n\n| Plan | Use Case |\n|------|----------|\n| **Anonymous** | Unauthenticated requests, restricted to `general` category |\n| **Free** | Development and low-volume integrations |\n| **Pro** | Standard production workloads |\n| **Enterprise** | High-volume production |\n\n:::note Anonymous Access\nUnauthenticated requests are only accepted on `general` category endpoints (OAuth discovery, token issuance). Every other category requires authentication.\n:::\n\n### Anonymous Tier\n\nBaked into the gateway — always the same values.\n\n| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |\n|----------|-------:|------:|--------:|----------------:|---------------:|\n| general  | 100    | 1,000 | 10,000  | 30              | 20             |\n| file / converter / publisher / webhook / ai | 0 | 0 | 0 | 0 | 0 |\n\n### Free\n\n| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |\n|----------|-------:|------:|--------:|----------------:|---------------:|\n| general   |   100 |    500 |   5,000 |  10 |  5 |\n| file      |   100 |    500 |   5,000 |  10 |  5 |\n| converter |    10 |     50 |     200 |   5 |  3 |\n| publisher |    10 |     50 |     200 |   5 |  3 |\n| webhook   |    50 |    200 |   2,000 |   5 |  3 |\n\nIncluded: 3 webhook subscriptions, 1 team member, 100 MB storage, 200 AI credits a month.\n\n### Pro\n\n| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |\n|----------|-------:|------:|--------:|----------------:|---------------:|\n| general   | 3,600 | 50,000 | 500,000 | 120 | 20 |\n| file      | 3,600 | 50,000 | 500,000 | 120 | 20 |\n| converter |   100 |    500 |   5,000 |  10 |  5 |\n| publisher |   100 |    500 |   5,000 |  10 |  5 |\n| webhook   | 1,000 | 10,000 | 100,000 |  50 | 10 |\n\nIncluded: 25 webhook subscriptions, 10 team members, 5 GB storage, 2,000 AI credits a month.\n\n### Enterprise\n\n| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |\n|----------|-------:|------:|--------:|----------------:|---------------:|\n| general   | 36,000 | 500,000 | 5,000,000 | 500 | 100 |\n| file      | 36,000 | 500,000 | 5,000,000 | 500 | 100 |\n| converter |  1,000 |   5,000 |    50,000 |  50 |  20 |\n| publisher |  1,000 |   5,000 |    50,000 |  50 |  20 |\n| webhook   | 10,000 | 100,000 | 1,000,000 | 200 |  50 |\n\nIncluded: 100 webhook subscriptions, 50 team members, 50 GB storage, 20,000 AI credits a month, priority support.\n\n:::note Your live limits and current usage\n- **In the API** — every response carries the effective limits and remaining quota in the `RateLimit` and `RateLimit-Policy` headers (see [Rate Limit Headers](https://docs.typograph.nl/guides/api/rate-limiting#rate-limit-headers) below), and the [usage endpoint](https://docs.typograph.nl/guides/api/rate-limiting#usage-endpoint) returns every layer of every category at once.\n- **In the Portal** — a live view of consumption versus limit per category is available under **Organizations → _your organization_ → Usage** in the [Typograph Portal](https://portal.typograph.nl).\n\nLimits may differ from the numbers above if custom overrides are in effect on your organization.\n:::","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#usage-endpoint","section":"Usage Endpoint","audience":"api","headings":[],"text":"`GET /v1/rate-limits/organizations/{organizationId}/usage` returns the limits and current usage of every layer, per category, for an organization you are a member of. It takes a user token with the `subscription` or `subscription:read` scope, as `GET /v1/subscription/usage` does; without either it answers `403 insufficient_scope`. An app's token reaches only the organization it was granted for: another one answers `403 organization_not_bound` ([which organization a token reaches](https://docs.typograph.nl/guides/api/authentication#which-organization-a-token-reaches)). The call itself counts against the `general` quota of the organization your client belongs to.\n\n```bash\ncurl https://api.typograph.nl/v1/rate-limits/organizations/$ORG_ID/usage \\\n  -H \"Authorization: Bearer $USER_TOKEN\"\n```\n\n```json\n{\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"tier\": \"pro\",\n  \"categories\": {\n    \"publisher\": {\n      \"burst\": { \"limit\": 5, \"remaining\": 5, \"used\": 0 },\n      \"hourly\": { \"limit\": 100, \"remaining\": 87, \"used\": 13 },\n      \"daily\": { \"limit\": 500, \"remaining\": 450, \"used\": 50, \"reset_at\": \"<ISO 8601 timestamp>\" },\n      \"monthly\": { \"limit\": 5000, \"remaining\": 4000, \"used\": 1000, \"reset_at\": \"<ISO 8601 timestamp>\" }\n    }\n  }\n}\n```\n\nA category your plan blocks is left out. The gateway answers this endpoint itself, with its own error codes: a client token gets a `403` with `user_token_required` (not the `invalid_token_type` of the proxied routes), an organization you are not a member of a `403` with `not_organization_member`.","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#endpoint-categories","section":"Endpoint Categories","audience":"api","headings":["AI credits"],"text":"Rate limits are applied per category based on the endpoint path:\n\n| Category | Endpoints | Description |\n|----------|-----------|-------------|\n| **general** | `/v1/identity/*`, `/oauth/*`, `/v1/document/*`, `/v1/rate-limits/*`, `/` | Standard API calls |\n| **converter** | `/v1/converter/*` | Format conversion (heavy resources) |\n| **publisher** | `/v1/publisher/*` | PDF generation (heavy resources) |\n| **file** | `/v1/file/*` | File and storage operations |\n| **webhook** | `/v1/webhook/*` | Webhook management |\n| **ai** | The portal's AI assistant | The assistant's requests; its routes are for the portal only, not part of the public API |\n\nThe `ai` category has the same limits on Free, Pro and Enterprise, not listed here since an integration never spends them, and no allowance at all for anonymous requests (0, as in the [Anonymous Tier](https://docs.typograph.nl/guides/api/rate-limiting#anonymous-tier) table). It is in the usage responses all the same: in the [usage endpoint](https://docs.typograph.nl/guides/api/rate-limiting#usage-endpoint) and in `GET /v1/subscription/usage`.\n\n### AI credits\n\nWhat the portal's AI assistant does is metered in **AI credits**, a monthly allowance per organization: 500 on Free, 5,000 on Pro and 20,000 on Enterprise (`quotas.ai_credits_monthly` of the plan). Pro and Enterprise can add credit bundles of 5,000 or 15,000 credits to the current month. AI credits are not requests and have no rate limits, but an organization uses at most a quarter of its month's credits, bundles included, in a UTC day. `GET /v1/subscription/usage` reports them as `ai_credits`, in `usage` the credits used this month and in `monthly_limits` the credits the organization has, with any custom limit or add-on applied; `0` is no AI credits, never unlimited. They reset with the calendar month, like the monthly request limits. What the assistant spends them on, and its own limits: [Assistant → AI Credits](https://docs.typograph.nl/portal/workspace/assistant#ai-credits).\n\n`GET /v1/subscription/usage/ai-credits` (`subscription` / `subscription:read`, a user token) shows them in more detail, for the organization named in the `X-Typograph-On-Behalf-Of-Organization-Id` header: the month and the day, and who used them.\n\n```json\n{\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"period\": \"<YYYY-MM>\",\n  \"period_ends_at\": \"<ISO 8601 timestamp>\",\n  \"resets_at\": \"<ISO 8601 timestamp>\",\n  \"limit\": 12000,\n  \"used\": 9900,\n  \"left\": 2100,\n  \"daily_limit\": 3000,\n  \"daily_used\": 900,\n  \"daily_left\": 2100,\n  \"daily_resets_at\": \"<ISO 8601 timestamp>\",\n  \"people\": [\n    {\"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\", \"name\": \"Grace Hopper\", \"is_guest\": false, \"used\": 5400},\n    {\"user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec07\", \"name\": \"Ada Lovelace\", \"is_guest\": true, \"used\": 4500}\n  ],\n  \"alert\": {\"threshold\": 80, \"top_user_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\", \"top_user_name\": \"Grace Hopper\", \"top_used\": 5400}\n}\n```\n\nThe owner and admins get everyone in `people`, guests included (`is_guest`: people with a role on the organization's teams who are not members); any other member gets only their own row. `used` is always the organization's total. `alert` is set once 80% of the month is used, and is `null` before that; for the owner and admins it names who used the most, for anyone else its `top_user_id`, `top_user_name` and `top_used` are `null`. A `name` is `null` when it is not known, for someone who has since left. `resets_at` and `daily_resets_at` are in UTC.","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#rate-limit-layers","section":"Rate Limit Layers","audience":"api","headings":["Layer 1: Burst Protection (Token Bucket)","Layer 2: Hourly Quota (Sliding Window)","Layer 3: Daily Quota (Fixed Window)","Layer 4: Monthly Quota (Fixed Window)"],"text":"The API uses four layers of rate limiting:\n\n### Layer 1: Burst Protection (Token Bucket)\n\nPrevents traffic spikes by limiting requests per minute with a burst capacity.\n\n- **How it works**: Tokens refill continuously at the specified rate\n- **Burst capacity**: Allows short bursts above the sustained rate\n- **Typical wait**: Seconds when exceeded\n\n### Layer 2: Hourly Quota (Sliding Window)\n\nEnforces the sustained hourly rate limit using a sliding window algorithm.\n\n- **Window**: 1 hour rolling window\n- **Algorithm**: Sliding window (no boundary burst issues)\n- **Typical wait**: Minutes to an hour when exceeded\n\n### Layer 3: Daily Quota (Fixed Window)\n\nLimits total requests per calendar day (UTC).\n\n- **Window**: Resets at UTC midnight\n- **Available**: Pro tier and above\n- **Typical wait**: Hours when exceeded\n\n### Layer 4: Monthly Quota (Fixed Window)\n\nLimits total requests per calendar month.\n\n- **Window**: Resets on the 1st of each month (UTC)\n- **Available**: Pro tier and above\n- **Typical wait**: Days when exceeded","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#rate-limit-headers","section":"Rate Limit Headers","audience":"api","headings":["Response Headers","Example Response Headers","Header Format Explained"],"text":"The API uses [IETF-standard rate limit headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/) to communicate quota status.\n\n### Response Headers\n\n| Header | Description | Format |\n|--------|-------------|--------|\n| `RateLimit` | Current limit status | `\"category\";r=remaining;t=reset_seconds` |\n| `RateLimit-Policy` | Configured policies | Comma-separated policy definitions |\n| `Retry-After` | Seconds until retry (when limited) | Integer seconds |\n\n### Example Response Headers\n\n```http\nHTTP/1.1 200 OK\nContent-Type: application/json\nRateLimit: \"general\";r=3542;t=1800\nRateLimit-Policy: \"general_hourly\";q=3600;w=3600, \"general_burst\";q=120;w=60, \"general_daily\";q=50000;w=86400\nX-Typograph-Request-Id: 019b28fb-a11e-7641-a28f-e978f892ec06\n```\n\n### Header Format Explained\n\n**RateLimit Header:**\n- `\"general\"` - The endpoint category\n- `r=3542` - Remaining requests in current window\n- `t=1800` - Seconds until the window resets\n\n**RateLimit-Policy Header:**\n- `q` - Quota (maximum requests allowed)\n- `w` - Window size in seconds (3600 = 1 hour, 86400 = 1 day)","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#rate-limit-exceeded","section":"Rate Limit Exceeded","audience":"api","headings":["Hourly Limit Exceeded","Burst Limit Exceeded","Daily Quota Exceeded","Monthly Quota Exceeded","Too Many Invalid Tokens"],"text":"### Hourly Limit Exceeded\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRateLimit: \"general\";r=0;t=1800\nRetry-After: 1800\n```\n\n```json\n{\n  \"error\": \"rate_limit_exceeded\",\n  \"error_description\": \"Hourly rate limit exceeded for general endpoints. Limit: 3600 requests per hour.\"\n}\n```\n\n### Burst Limit Exceeded\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: 5\n```\n\n```json\n{\n  \"error\": \"burst_rate_limit_exceeded\",\n  \"error_description\": \"Burst rate limit exceeded for general endpoints. Too many requests in a short time period.\"\n}\n```\n\n### Daily Quota Exceeded\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: 28800\n```\n\n```json\n{\n  \"error\": \"daily_quota_exceeded\",\n  \"error_description\": \"Daily quota exceeded for converter endpoints. Limit: 2000 requests.\"\n}\n```\n\n### Monthly Quota Exceeded\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: 172800\n```\n\n```json\n{\n  \"error\": \"monthly_quota_exceeded\",\n  \"error_description\": \"Monthly quota exceeded for converter endpoints. Limit: 20000 requests.\"\n}\n```\n\n### Too Many Invalid Tokens\n\nA request with an invalid or expired access token is answered with `401 invalid_token`. An address that keeps sending them — more than 30 in a short burst, or more than 300 in an hour — gets `429` on every endpoint that takes a token, even with a valid token. A block lasts at most a minute, also once the hourly limit is spent; after it a valid token works again, while another invalid one blocks the address for the next minute until the hour allows more. `Retry-After` says how long to wait. An IPv6 address counts together with the rest of its /64 network. An integration that refreshes its token when it gets a `401` never comes near this limit.\n\n```http\nHTTP/1.1 429 Too Many Requests\nContent-Type: application/json\nRetry-After: 2\n```\n\n```json\n{\n  \"error\": \"authentication_rate_limit_exceeded\",\n  \"error_description\": \"Too many requests with an invalid or expired token. Try again later.\"\n}\n```\n\nAnonymous requests, such as token requests to `POST /oauth/token`, are counted per address the same way, against the [Anonymous Tier](https://docs.typograph.nl/guides/api/rate-limiting#anonymous-tier).","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#handling-rate-limits","section":"Handling Rate Limits","audience":"api","headings":["Parse IETF Headers","Implement Exponential Backoff","Queue and Throttle Requests"],"text":"### Parse IETF Headers\n\n```javascript\nfunction parseRateLimitHeader(response) {\n  const rateLimit = response.headers.get('RateLimit');\n  if (!rateLimit) return null;\n\n  // Parse: \"category\";r=remaining;t=reset_seconds\n  const match = rateLimit.match(/\"([^\"]+)\";r=(\\d+);t=(\\d+)/);\n  if (!match) return null;\n\n  return {\n    category: match[1],\n    remaining: parseInt(match[2], 10),\n    resetSeconds: parseInt(match[3], 10)\n  };\n}\n\nasync function makeRequest(url, options) {\n  const response = await fetch(url, options);\n  const limits = parseRateLimitHeader(response);\n\n  if (limits && limits.remaining < 100) {\n    console.warn(`Rate limit warning: ${limits.remaining} requests remaining for ${limits.category}`);\n  }\n\n  return response;\n}\n```\n\n### Implement Exponential Backoff\n\n```javascript\nasync function requestWithRetry(url, options, maxRetries = 3) {\n  for (let attempt = 0; attempt < maxRetries; attempt++) {\n    const response = await fetch(url, options);\n\n    if (response.status !== 429) {\n      return response;\n    }\n\n    const retryAfter = parseInt(response.headers.get('Retry-After'), 10) || 60;\n    const delay = Math.min(retryAfter * 1000, Math.pow(2, attempt) * 1000);\n\n    console.log(`Rate limited. Waiting ${delay}ms before retry...`);\n    await new Promise(resolve => setTimeout(resolve, delay));\n  }\n\n  throw new Error('Max retries exceeded');\n}\n```\n\n### Queue and Throttle Requests\n\nFor batch operations, queue requests and process them at a sustainable rate:\n\n```javascript\nclass RateLimitedQueue {\n  constructor(requestsPerSecond = 1) {\n    this.queue = [];\n    this.interval = 1000 / requestsPerSecond;\n    this.processing = false;\n  }\n\n  async add(fn) {\n    return new Promise((resolve, reject) => {\n      this.queue.push({ fn, resolve, reject });\n      this.process();\n    });\n  }\n\n  async process() {\n    if (this.processing || this.queue.length === 0) return;\n\n    this.processing = true;\n    const { fn, resolve, reject } = this.queue.shift();\n\n    try {\n      const result = await fn();\n      resolve(result);\n    } catch (error) {\n      reject(error);\n    }\n\n    setTimeout(() => {\n      this.processing = false;\n      this.process();\n    }, this.interval);\n  }\n}\n\n// Usage: Process at 1 request per second\nconst queue = new RateLimitedQueue(1);\nconst results = await Promise.all(\n  jobIds.map(id => queue.add(() => fetchJob(id)))\n);\n```","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#best-practices","section":"Best Practices","audience":"api","headings":["Cache Responses","Use Webhooks Instead of Polling","Separate Clients by Category","Avoid Traffic Spikes"],"text":"### Cache Responses\n\nCache API responses to reduce the number of requests:\n\n```javascript\nconst cache = new Map();\nconst CACHE_TTL = 60000; // 1 minute\n\nasync function getCachedTeams(accessToken) {\n  const cacheKey = 'teams';\n  const cached = cache.get(cacheKey);\n\n  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {\n    return cached.data;\n  }\n\n  const response = await fetch('https://api.typograph.nl/v1/file/teams', {\n    headers: { 'Authorization': `Bearer ${accessToken}` }\n  });\n\n  const data = await response.json();\n  cache.set(cacheKey, { data, timestamp: Date.now() });\n\n  return data;\n}\n```\n\n### Use Webhooks Instead of Polling\n\nInstead of polling for job status, use webhooks to receive notifications:\n\n```javascript\n// Instead of polling every second\nsetInterval(async () => {\n  const job = await getJob(jobId);\n  if (job.status === 'completed') {\n    // Handle completion\n  }\n}, 1000);\n\n// Use webhooks\napp.post('/webhooks/typograph', (req, res) => {\n  const { event, job } = req.body;\n  if (job.status === 'completed') {\n    // Handle completion\n  }\n  res.status(200).json({ received: true });\n});\n```\n\n### Separate Clients by Category\n\nIf you have high usage in specific categories, consider using separate OAuth clients:\n\n```javascript\nconst clients = {\n  general: { clientId: '...', accessToken: '...' },\n  converter: { clientId: '...', accessToken: '...' },\n  publisher: { clientId: '...', accessToken: '...' }\n};\n\nasync function convertDocument(file) {\n  return fetch('https://api.typograph.nl/v1/converter/jobs', {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${clients.converter.accessToken}`\n    },\n    body: file\n  });\n}\n```\n\n### Avoid Traffic Spikes\n\nTo avoid hitting burst limits:\n\n1. **Spread requests evenly** - Don't send many requests at once\n2. **Add small delays** - Add 100-500ms delays between batch requests\n3. **Use queuing** - Process requests through a rate-limited queue\n4. **Monitor headers** - Watch `RateLimit` remaining values closely","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#error-codes-summary","section":"Error Codes Summary","audience":"api","headings":[],"text":"| Error Code | Description | Typical Retry Time |\n|------------|-------------|-------------------|\n| `burst_rate_limit_exceeded` | Too many requests in short period | Seconds |\n| `rate_limit_exceeded` | Hourly quota exceeded | Minutes to an hour |\n| `daily_quota_exceeded` | Daily quota exceeded | Hours |\n| `monthly_quota_exceeded` | Monthly quota exceeded | Days |","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#upgrading-your-plan","section":"Upgrading Your Plan","audience":"api","headings":[],"text":"Pro and Enterprise are on request: the API answers their `price_monthly` and `price_yearly` as `null`. A plan is not changed through the API.\n\n- **Pro**: ask for it in the [Typograph Portal](https://portal.typograph.nl), on your organization's Subscription page under **Change plan**, or mail [sales@typograph.nl](mailto:sales@typograph.nl). Sales agrees the price with you and then moves your organization.\n- **Enterprise**: contact [sales@typograph.nl](mailto:sales@typograph.nl) for pricing and custom rate-limit overrides.","agent_tools":[]},{"title":"Rate Limiting","url":"https://docs.typograph.nl/guides/api/rate-limiting#custom-rate-limits","section":"Custom Rate Limits","audience":"api","headings":[],"text":"Enterprise accounts can request custom rate limits per category. Contact [support](https://docs.typograph.nl/guides/support) with:\n\n1. Your organization ID\n2. The categories that need custom limits\n3. Desired hourly / daily / monthly quotas\n4. Rationale for the increase\n\nApproved overrides apply to every OAuth client in the organization and take effect within 5 minutes (the gateway's limit cache TTL).","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names","section":null,"audience":"api","headings":[],"text":"Typograph names everything on the wire by one convention: a stated boolean starts with `is_`, `has_`\nor `can_`, a timestamp ends in `_at`, a count is `<noun>_count`, a reference into the template\ndocument ends in `_uuid`, and names are written in full. Moving to it renamed the names below **in\none release, without a transition**: the API sends and accepts only the new name, so an integration\nthat reads or sends an old one must switch.\n\nTwo places still read the old name, as a deprecated alias, because stored data and customers'\npackages carry it:\n\n- **Publisher requests**: `spreads_as_single_pages` (now `single_pages`) and `merge.file_name` (now\n  `merge.filename`). When both are sent, the new name wins.\n- **Template JSON and template packages**: every old key, and a package's `templatebundle.json` (now\n  `template_bundle.json`). The template keys are listed under [Older key names](https://docs.typograph.nl/guides/concepts/documents#older-key-names).","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#paths","section":"Paths","audience":"api","headings":[],"text":"| Old | New |\n|---|---|\n| `/v1/file/{resourceType}/{resourceId}/invites[/{id}]` | `/v1/file/{resourceType}/{resourceId}/invitations[/{id}]` |\n| `/v1/file/users/me/invites` | `/v1/file/users/me/invitations` |\n| `/v1/file/templates/{id}/collab-token` | `/v1/file/templates/{id}/collaboration-token` |\n| `/v1/file/recent/searched` | `/v1/file/recent/searches` |","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#file-service","section":"File service","audience":"api","headings":[],"text":"| Old | New | Where |\n|---|---|---|\n| `resolved`, `?resolved=` | `is_resolved`, `?is_resolved=` | comment, resolve a comment |\n| `expired` | `is_expired` | every presigned URL |\n| `featured`, `?featured=` | `is_featured`, `?is_featured=` | pre-made templates |\n| `usable` | `is_usable` | pre-made templates |\n| `exists` | `is_found` | font lookup |\n| `saved` | `is_saved` | touch a template |\n| `complete` | `is_complete` | form values |\n| `from`, `params` | `sender`, `parameters` | notification |\n| `targets[].uuid` | `targets[].element_uuid` | template fields |\n| `element_id` | `element_uuid` | form fields |\n| `label_name` | `label` | update a font |\n| `dev_url` | `development_url` | template |\n| `thumbnail` | `thumbnail_filename` | shared item |\n| `total_templates`, `total_users` | `template_count`, `user_count` | project, team, shared item |\n| `?trash=` | `?include_deleted=` | search |\n| `propertyPath` | `property_path` | a `422`'s violations (also the converter and the publisher) |","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#identity-service-and-the-api-home","section":"Identity service and the API home","audience":"api","headings":[],"text":"| Old | New | Where |\n|---|---|---|\n| `expired` | `is_expired` | organization invitation |\n| `deletion_scheduled_for` | `deletion_scheduled_at` | organization |\n| `my_can_use_quota`, `my_role` | `can_use_quota`, `role` | organization |\n| `website` | `website_url` | organization, create and update |\n| `auth`, `portal`, `website` | `authentication`, `portal_url`, `website_url` | `GET /` |","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#publisher-subscription-and-webhooks","section":"Publisher, subscription and webhooks","audience":"api","headings":[],"text":"| Old | New | Where |\n|---|---|---|\n| `valid`, `plan.proof` | `is_valid`, `plan.is_proof` | merge check |\n| `rows_total`, `rows_unchecked` | `row_count`, `unchecked_row_count` | merge check |\n| `rows_total`, `rows_done`, `rows_failed` | `row_count`, `printed_row_count`, `failed_row_count` | merge job and its `publication.*` events |\n| `current_period_start`, `current_period_end` | `current_period_starts_at`, `current_period_ends_at` | subscription |\n| `period_start`, `period_end` | `period_starts_at`, `period_ends_at` | usage |\n| `attempts` | `attempt_count` | webhook delivery |","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#mcp-server","section":"MCP server","audience":"api","headings":[],"text":"| Old | New |\n|---|---|\n| tools `invite_create`, `invite_list_my`, `invite_list_resource`, `invite_accept`, `invite_decline`, `invite_delete` | `invitation_create`, `invitation_list_my`, `invitation_list_resource`, `invitation_accept`, `invitation_decline`, `invitation_delete` |\n| argument `invite_id` | `invitation_id` |\n| arguments `featured`, `website`, `font_update.name` | `is_featured`, `website_url`, `label` |\n| results `deleted`, `restored`, `touched`, `accepted`, `declined`, `revoked`, `removed`, `marked_read`, `marked_unread`, `marked_all_read`, `code_sent` | gone: `success` says it |\n| results `download_available`, `valid`, `proof` | `is_download_available`, `is_valid`, `is_proof` |\n| results `deletion_scheduled_for`, `website`, `earliest_expiry` | `deletion_scheduled_at`, `website_url`, `earliest_expires_at` |\n\nEvery other result follows the API's names, booleans included (`is_active`, `is_featured`, `is_expired`).","agent_tools":[]},{"title":"Renamed fields and paths","url":"https://docs.typograph.nl/guides/api/renamed-names#webshop-api","section":"Webshop API","audience":"api","headings":[],"text":"| Old | New |\n|---|---|\n| `default` (language) | `is_default` |\n| `works_in_the_back_office` | `is_staff` |\n| `enabled` (feedback) | `is_enabled` |\n| `personalizable` | `is_personalizable` |\n| `chosen` (facet value) | `is_selected` |\n| `downloadable` (print file) | `is_downloadable` |\n| `print_spec` | `print_specification` |\n| `?q=` | `?query=` |\n| `favourites`, `/users/me/favourites/{slug}`, `too_many_favourites` | `favorites`, `/users/me/favorites/{slug}`, `too_many_favorites` |\n| order files' `element_id` | `element_uuid` |","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes","section":null,"audience":"api","headings":[],"text":"Scopes define which parts of the API your application can access. Request only the scopes your application needs.","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#available-scopes","section":"Available Scopes","audience":"api","headings":["OpenID Connect Scopes","Service Scopes","System Scopes"],"text":"### OpenID Connect Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `openid` | Enable OpenID Connect authentication |\n| `profile` | Access user profile (name, handle) |\n| `email` | Access user email address |\n| `offline_access` | Receive refresh tokens for token renewal |\n\n### Service Scopes\n\n| Scope | Description | Endpoints |\n|-------|-------------|-----------|\n| `identity` | Full user identity management | `/v1/identity/*` |\n| `identity:read` | Read-only user profile access | `/v1/identity/users/me`, `/oauth/userinfo` |\n| `identity:write` | Update user profile and manage organizations | `/v1/identity/*` (write operations) |\n| `identity:invitations` | Invite people to an organization, send an invitation again and revoke one | `/v1/identity/organizations/{id}/invitations` (POST), `/v1/identity/organizations/{id}/invitations/{invitationId}/resend` (POST), `/v1/identity/organizations/{id}/invitations/{invitationId}` (DELETE) |\n| `identity:delete` | Delete the user's own account, or an organization they own, with a code from their mailbox; not included in `identity` | `/v1/identity/users/me/deletion-code` (POST), `/v1/identity/users/me` (DELETE), `/v1/identity/organizations/{id}/deletion-code` (POST), `/v1/identity/organizations/{id}` (DELETE) |\n| `file` | Full file service access | `/v1/file/*` |\n| `file:read` | Read-only file access | `/v1/file/*` (GET only) |\n| `file:write` | Create and update files | `/v1/file/*` (POST, PATCH, PUT) |\n| `file:delete` | Delete files and resources | `/v1/file/*` (DELETE) |\n| `media` | Full media library access (subset of `file`) | `/v1/file/projects/{id}/media*` |\n| `media:read` | Read-only access to media files | `/v1/file/projects/{id}/media` (GET) |\n| `media:write` | Update and move media files | `/v1/file/projects/{id}/media/{id}` (PATCH), `/v1/file/media/{id}/move` (PATCH) |\n| `media:delete` | Delete media files | `/v1/file/projects/{id}/media/{id}` (DELETE) |\n| `font` | Full font library access (subset of `file`) | `/v1/file/projects/{id}/fonts*` |\n| `font:read` | Read-only access to font files | `/v1/file/projects/{id}/fonts` (GET) |\n| `font:write` | Update and move font files | `/v1/file/projects/{id}/fonts/{id}` (PATCH), `/v1/file/fonts/{id}/move` (PATCH) |\n| `font:delete` | Delete font files | `/v1/file/projects/{id}/fonts/{id}` (DELETE) |\n| `document` | Full document operations | `/v1/document/*` |\n| `document:read` | Read-only document access | `/v1/document/*` (GET, validation) |\n| `document:write` | Create and update documents | `/v1/document/*` (POST, PATCH) |\n| `publisher` | Full publisher access | `/v1/publisher/*` |\n| `publisher:read` | View publication jobs | `/v1/publisher/*` (GET) |\n| `publisher:write` | Create publication jobs | `/v1/publisher/*` (POST) |\n| `converter` | Full converter access | `/v1/converter/*` |\n| `converter:read` | View conversion jobs | `/v1/converter/*` (GET) |\n| `converter:write` | Create conversion jobs | `/v1/converter/*` (POST) |\n| `webhook` | Full webhook management | `/v1/webhook/*` |\n| `webhook:read` | View webhook subscriptions | `/v1/webhook/*` (GET) |\n| `webhook:write` | Create and update webhooks | `/v1/webhook/*` (POST, PATCH) |\n| `webhook:delete` | Delete webhook subscriptions | `/v1/webhook/*` (DELETE) |\n| `subscription` | Full subscription access | `/v1/subscription/*`, `/v1/rate-limits/organizations/{organizationId}/usage` |\n| `subscription:read` | View subscription and usage | `/v1/subscription/*` (GET), `/v1/rate-limits/organizations/{organizationId}/usage` |\n| `subscription:write` | Buy and cancel add-ons, in the portal only | none for apps: those routes answer `403 first_party_only` |\n| `authorization` | OAuth client management | `/v1/identity/oauth/*` |\n\n### System Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `authorization` | Manage authentication tokens |\n| `health` | Health check endpoints |","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#scope-hierarchy","section":"Scope Hierarchy","audience":"api","headings":[],"text":"Parent scopes automatically grant all their child scopes. A token with a parent scope can access any endpoint that requires one of its child scopes.\n\n| Parent Scope | Grants |\n|-------------|--------|\n| `identity` | `identity:read`, `identity:write`, `identity:invitations` |\n| `file` | `file:read`, `file:write`, `file:delete` |\n| `media` | `media:read`, `media:write`, `media:delete` |\n| `font` | `font:read`, `font:write`, `font:delete` |\n| `document` | `document:read`, `document:write` |\n| `publisher` | `publisher:read`, `publisher:write` |\n| `converter` | `converter:read`, `converter:write` |\n| `webhook` | `webhook:read`, `webhook:write`, `webhook:delete` |\n| `subscription` | `subscription:read`, `subscription:write` |\n\n**Example:** A token with `file` scope can access all file endpoints — GET (read), POST/PATCH (write), and DELETE (delete) operations.","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#deprecated-scopes","section":"Deprecated Scopes","audience":"api","headings":[],"text":"The following scopes have been renamed and are accepted as aliases during the transition period:\n\n| Old Scope | New Scope | Notes |\n|-----------|-----------|-------|\n| `publisher:publish` | `publisher:write` | Use `publisher:write` in new applications |\n| `converter:convert` | `converter:write` | Use `converter:write` in new applications |\n\nThe deprecated names will continue to work but may be removed in a future version.","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#scope-details","section":"Scope Details","audience":"api","headings":["openid","profile","email","offline_access","identity","identity:read","identity:write","identity:invitations","identity:delete","file","file:read","file:write","file:delete","media","media:read","media:write","media:delete","font","font:read","font:write","font:delete","document","document:read","document:write","publisher","publisher:read","publisher:write","converter","converter:read","converter:write","webhook","webhook:read","webhook:write","webhook:delete","subscription","subscription:read","subscription:write","authorization"],"text":"### `openid`\n\nEnables OpenID Connect authentication. When included:\n- The `/oauth/userinfo` endpoint becomes accessible\n- ID tokens include the `sub` (subject) claim\n\n**Required for:** User identification, SSO integrations\n\n### `profile`\n\nAccess the user's public profile information:\n- Display name\n- Handle (username)\n\n**Required for:** Personalizing user experience\n\n### `email`\n\nAccess the user's email address:\n- Email address\n- Email verification status\n\n**Required for:** User communication, account linking\n\n### `offline_access`\n\nReceive refresh tokens with access tokens, enabling:\n- Token renewal without user interaction\n- Long-running background processes\n\n**Required for:** Background jobs, persistent sessions\n\n### `identity`\n\nFull access to user identity operations:\n- Read and update user profile\n- Manage user settings\n- Access user information\n- Manage organizations\n\n**Grants:** `identity:read`, `identity:write`, `identity:invitations`\n\n**Required for:** User management features\n\n### `identity:read`\n\nRead-only access to user profile:\n- View user information\n- Cannot modify identity settings\n\n**Required for:** Profile display, read-only integrations\n\n### `identity:write`\n\nWrite access to user identity:\n- Update user profile information\n- Manage organization settings\n\n**Required for:** Profile editing, user management\n\n### `identity:invitations`\n\nManaging an organization's invitations, and nothing else:\n- Invite an email address into the organization\n- Send an invitation again, and revoke one\n\nNarrower than `identity:write`: it changes no profile, role or setting itself. An invitation\nas `admin` does make the person an admin once they accept, as in the portal. Only the\norganization's owner and admins invite, whatever the token holds. To list the invitations, ask for\n`identity:read` as well. See [Organization invitations](https://docs.typograph.nl/guides/api/identity-service#organization-invitations).\n\n**Required for:** Inviting people from your app, without the rest of `identity:write`\n\n### `identity:delete`\n\nDeleting the signed-in user's own account, or an organization they own:\n- Mail the user the code that confirms it\n- Schedule the account for deletion, or delete the organization, with that code\n\n`identity` does not grant it: ask for it on its own, so the user sees it when they approve your\napp. Every plan includes it, and it works only with the code the user reads from their own\nmailbox. See [Delete the current user's account](https://docs.typograph.nl/guides/api/identity-service#delete-the-current-users-account)\nand [Delete an organization](https://docs.typograph.nl/guides/api/identity-service#delete-an-organization).\n\n**Required for:** Letting a user delete their account, or an organization they own, from your app\n\n### `file`\n\nFull access to the File Service:\n- Create, read, update, delete teams\n- Manage projects and templates\n- Upload and download files\n- Manage media and fonts\n- Handle permissions and sharing\n\n**Grants:** `file:read`, `file:write`, `file:delete`\n\n**Required for:** Full file management, template editing\n\n### `file:read`\n\nRead-only access to the File Service:\n- View teams, projects, templates\n- Download files and media\n- Cannot create or modify resources\n\n**Required for:** Template rendering, read-only viewers\n\n### `file:write`\n\nWrite access to the File Service:\n- Create and update teams, projects, templates\n- Upload files and media\n- Cannot delete resources\n\n**Required for:** Content creation and editing\n\n### `file:delete`\n\nDelete access to the File Service:\n- Delete teams, projects, templates\n- Remove media and font files\n\n**Required for:** Full content management\n\n### `media`\n\nFull access to the media library only — a narrower alternative to `file` when an integration should not touch templates, projects, or fonts. Grants `media:read`, `media:write`, and `media:delete`.\n\n**Required for:** Media-focused integrations (DAM systems, image pipelines)\n\n### `media:read`\n\nRead-only access to a project's media library:\n- List media files in a project\n- Request presigned URLs to download media\n\n**Required for:** Read-only media viewers and pickers\n\n### `media:write`\n\nUpdate media files in a project, move them to another project, and restore them from the trash. The presigned-URL endpoint only hands out read links, so it takes `media:read`; uploading goes through the project's uploads, with `file:write`.\n\n**Required for:** Apps that organise a user's media library\n\n### `media:delete`\n\nDelete media files from a project.\n\n**Required for:** Apps that manage a user's media library\n\n### `font`\n\nFull access to the font library only — a narrower alternative to `file` when an integration should not touch templates, projects, or media. Grants `font:read`, `font:write`, and `font:delete`.\n\n**Required for:** Font management integrations\n\n### `font:read`\n\nRead-only access to a project's fonts:\n- List fonts in a project\n- Request presigned URLs to download fonts\n\n**Required for:** Read-only font browsing\n\n### `font:write`\n\nUpdate font files in a project, move them to another project, and restore them from the trash. The presigned-URL endpoint only hands out read links, so it takes `font:read`; uploading goes through the project's uploads, with `file:write`.\n\n**Required for:** Apps that organise a user's font library\n\n### `font:delete`\n\nDelete font files from a project.\n\n**Required for:** Apps that manage a user's font library\n\n### `document`\n\nFull access to the Document Service:\n- Validate Typograph templates\n- Upgrade templates to newer schema versions\n- Check template compatibility\n\n**Grants:** `document:read`, `document:write`\n\n**Required for:** Template validation, schema migration\n\n### `document:read`\n\nRead-only document access:\n- Validate templates\n- Check compatibility\n\n**Required for:** Template validation\n\n### `document:write`\n\nWrite access to documents:\n- Upgrade template versions\n\n**Required for:** Schema migration\n\n### `publisher`\n\nFull access to the Publisher Service:\n- Create document generation jobs\n- Generate PDFs and images from templates\n- Process CSV data for batch generation\n- Download generated files\n\n**Grants:** `publisher:read`, `publisher:write`\n\n**Required for:** Document generation, PDF creation\n\n### `publisher:read`\n\nRead-only publisher access:\n- List and view generation job details\n- Download generated files\n\n**Required for:** Monitoring jobs, downloading results\n\n### `publisher:write`\n\nWrite access to the publisher:\n- Create document generation jobs\n\n**Required for:** Triggering document generation\n\n### `converter`\n\nFull access to the Converter Service:\n- Convert InDesign packages to Typograph format\n- Convert between font formats (TTF, OTF, WOFF, WOFF2)\n- Convert and resize images\n\n**Grants:** `converter:read`, `converter:write`\n\n**Required for:** InDesign import, format conversion\n\n### `converter:read`\n\nRead-only converter access:\n- List and view conversion job details\n- Download converted files\n\n**Required for:** Monitoring conversion jobs\n\n### `converter:write`\n\nWrite access to the converter:\n- Create conversion jobs\n\n**Required for:** Triggering file conversions\n\n### `webhook`\n\nFull access to the Webhook Service:\n- Create and manage webhook subscriptions\n- View delivery history\n- Retry failed deliveries\n- Delete subscriptions\n\n**Grants:** `webhook:read`, `webhook:write`, `webhook:delete`\n\n**Required for:** Event-driven integrations\n\n### `webhook:read`\n\nRead-only webhook access:\n- View webhook subscriptions\n- View delivery logs\n\n**Required for:** Monitoring webhooks\n\n### `webhook:write`\n\nWrite access to webhooks:\n- Create and update webhook subscriptions\n- Retry deliveries\n\n**Required for:** Setting up webhook integrations\n\n### `webhook:delete`\n\nDelete access to webhooks:\n- Delete webhook subscriptions\n\n**Required for:** Managing webhook lifecycle\n\n### `subscription`\n\nFull access to subscription management:\n- View subscription plan and usage\n- Buy and cancel add-ons, and cancel and reactivate subscriptions: in the portal only,\n  by the organization's owner. They change the contract and the bill, so an app's\n  token gets `403 first_party_only`, and any other member, admins included, gets `403`\n\n**Grants:** `subscription:read`, `subscription:write`\n\n**Required for:** Subscription management\n\n### `subscription:read`\n\nRead-only subscription access:\n- View current subscription plan\n- View usage statistics, and the rate-limit usage per category (`GET /v1/rate-limits/organizations/{organizationId}/usage`)\n- View available add-ons\n\n**Required for:** Displaying subscription info\n\n### `subscription:write`\n\nWrite access to subscriptions:\n- Buy and cancel add-ons: in the portal only, by the organization's owner. An app's\n  token gets `403 first_party_only`\n\n**Required for:** Subscription management in the portal\n\n### `authorization`\n\nManage OAuth applications:\n- Create and configure OAuth clients\n- View registered applications\n- Manage redirect URIs and scopes\n\n**Required for:** OAuth client management","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#requesting-scopes","section":"Requesting Scopes","audience":"api","headings":["Multiple Scopes","Using Parent Scopes","Requesting only what you need","Scope Limitations","Organization-Based Scope Validation"],"text":"When obtaining an access token, specify the scopes your application needs:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=file document publisher\"\n```\n\n### Multiple Scopes\n\nRequest multiple scopes by separating them with spaces:\n\n```\nscope=openid profile email file document publisher\n```\n\n### Using Parent Scopes\n\nRequest a parent scope to get all child permissions:\n\n```\nscope=file publisher converter\n```\n\nThis is equivalent to requesting:\n\n```\nscope=file:read file:write file:delete publisher:read publisher:write converter:read converter:write\n```\n\n### Requesting only what you need\n\nThe `media:*` and `font:*` scope families exist so an integration can be granted access to just a user's media or font library, without also exposing templates, projects, or teams. Prefer them over the broader `file:*` family whenever your application's scope is narrower.\n\n```\n# Example: an app that only manages the user's media library\nscope=media:read media:write media:delete openid offline_access\n```\n\n### Scope Limitations\n\n- You can only request scopes that have been granted to your application\n- Requesting unauthorized scopes will result in an `invalid_scope` error\n- The token will only include scopes that are both requested and authorized\n\n### Organization-Based Scope Validation\n\nWhen a token is issued, the scopes are validated against the **organization's subscription**:\n\n**How it works:**\n1. Each OAuth client belongs to an organization\n2. The organization's subscription plan determines which scopes are available\n3. At token creation time, requested scopes are filtered to the organization's allowed scopes\n4. If the organization's subscription changes, the allowed scopes may change accordingly\n\n**Example:**\n```\nOrganization subscription:    Professional Plan\nOrganization allowed scopes:  [file, document, publisher, converter, subscription:read]\nClient allowed scopes:        [file, document]\nToken requested scopes:       [file, document]\nToken granted scopes:         [file, document] ✓\n```\n\nIf the organization downgrades to Free plan:\n```\nOrganization allowed scopes:  [file, document]  (publisher removed)\nClient allowed scopes:        [file, document, publisher]\nToken requested scopes:       [file, document, publisher]\nToken granted scopes:         [file, document] (publisher filtered out)\n```\n\n**Key points:**\n- Scopes are controlled at the **organization level**, not the user level\n- The **organization's subscription** determines which scopes are available\n- Users can freely use OAuth applications without personal scope restrictions\n- Upgrading your subscription unlocks additional scopes","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#common-scope-combinations","section":"Common Scope Combinations","audience":"api","headings":["Server-to-Server Integration","User-Facing Application","Read-Only Access","Document Generation Only","Webhook Consumer","Full Management"],"text":"### Server-to-Server Integration\n\nFor backend services that generate documents:\n\n```\nscope=file document publisher converter\n```\n\n### User-Facing Application\n\nFor applications where users authenticate:\n\n```\nscope=openid profile email file offline_access\n```\n\n### Read-Only Access\n\nFor viewers and display applications:\n\n```\nscope=file:read identity:read\n```\n\n### Document Generation Only\n\nFor services that only generate documents:\n\n```\nscope=publisher:write file:read\n```\n\n### Webhook Consumer\n\nFor event-driven applications:\n\n```\nscope=webhook file:read\n```\n\n### Full Management\n\nFor applications that need complete access:\n\n```\nscope=openid profile email identity file document publisher converter webhook subscription offline_access\n```","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#checking-granted-scopes","section":"Checking Granted Scopes","audience":"api","headings":["Token Response","Token Introspection","UserInfo Endpoint"],"text":"### Token Response\n\nThe token response includes the granted scopes:\n\n```json\n{\n  \"access_token\": \"typ_abc123...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 172800,\n  \"scope\": \"file document publisher\"\n}\n```\n\n### Token Introspection\n\nVerify scopes for an existing token:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/introspect \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=YOUR_ACCESS_TOKEN\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\"\n```\n\n### UserInfo Endpoint\n\nFor tokens with `openid` scope:\n\n```bash\ncurl https://api.typograph.nl/oauth/userinfo \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```","agent_tools":[]},{"title":"Scopes","url":"https://docs.typograph.nl/guides/api/scopes#scope-related-errors","section":"Scope-Related Errors","audience":"api","headings":["403 Forbidden","invalid_scope"],"text":"### 403 Forbidden\n\nReturned when your token lacks the required scope:\n\n```json\n{\n  \"error\": \"insufficient_scope\",\n  \"error_description\": \"Token does not have the required scope: publisher:write\"\n}\n```\n\n### invalid_scope\n\nReturned during token request when requesting unavailable scopes:\n\n```json\n{\n  \"error\": \"invalid_scope\",\n  \"error_description\": \"The requested scope 'subscription:write' is not available for this client\"\n}\n```","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types","section":null,"audience":"api","headings":[],"text":"The Typograph API uses two types of access tokens, each designed for different use cases. Understanding which token type to use is essential for successful API integration.","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#overview","section":"Overview","audience":"api","headings":[],"text":"| Token Type | Obtained Via | Has User Context | Use Case |\n|------------|--------------|------------------|----------|\n| **User Token** | Authorization Code flow | Yes (`sub` claim present) | User-facing applications |\n| **Client Token** | Client Credentials flow | No | Server-to-server integrations |","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#user-tokens","section":"User Tokens","audience":"api","headings":["Characteristics","When to Use","Obtaining a User Token","Introspection Response"],"text":"User tokens represent a specific user's authorization to your application. They are obtained through the **Authorization Code** flow, where a user explicitly grants your application access.\n\n### Characteristics\n\n- Contains a `sub` claim (user ID)\n- Can access all API endpoints\n- Actions are attributed to the user\n- Required for user-specific data\n\n### When to Use\n\n- Building user-facing applications\n- Accessing user-specific data (e.g., `/v1/identity/users/me`)\n- Performing actions on behalf of a user\n- Any operation that requires user context\n\n### Obtaining a User Token\n\n```bash\n# Step 1: Redirect user to authorization\nhttps://api.typograph.nl/oauth/authorize\n  ?response_type=code\n  &client_id=YOUR_CLIENT_ID\n  &redirect_uri=https://your-app.com/callback\n  &scope=openid profile file\n  &state=random_state\n  &code_challenge=CODE_CHALLENGE\n  &code_challenge_method=S256\n\n# Step 2: Exchange authorization code for tokens\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=authorization_code\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"code=AUTHORIZATION_CODE\" \\\n  -d \"redirect_uri=https://your-app.com/callback\" \\\n  -d \"code_verifier=CODE_VERIFIER\"\n```\n\n### Introspection Response\n\n```json\n{\n  \"active\": true,\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"token_type\": \"Bearer\",\n  \"grant_type\": \"authorization_code\",\n  \"auth_type\": \"user\",\n  \"scope\": \"openid profile file\",\n  \"sub\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n  \"exp\": 1734567890,\n  \"iat\": 1734395090\n}\n```","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#client-tokens","section":"Client Tokens","audience":"api","headings":["Characteristics","When to Use","Obtaining a Client Token","Introspection Response"],"text":"Client tokens represent your application itself, without any user context. They are obtained through the **Client Credentials** flow.\n\n### Characteristics\n\n- No `sub` claim (no user context)\n- Limited to specific endpoints\n- Actions are attributed to the application\n- Cannot access user-specific endpoints\n\n### When to Use\n\n- Server-to-server integrations\n- Background jobs and scheduled tasks\n- Webhook subscription management\n- Automated processing pipelines\n\n### Obtaining a Client Token\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\" \\\n  -d \"scope=webhook publisher converter\"\n```\n\n### Introspection Response\n\n```json\n{\n  \"active\": true,\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"token_type\": \"Bearer\",\n  \"grant_type\": \"client_credentials\",\n  \"auth_type\": \"client\",\n  \"scope\": \"webhook publisher converter\",\n  \"exp\": 1734567890,\n  \"iat\": 1734395090\n}\n```","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#endpoint-requirements","section":"Endpoint Requirements","audience":"api","headings":["Endpoints Accepting Either Token Type (any)","Endpoints Requiring User Tokens"],"text":"Token-type requirements are set **per route** at the gateway. There are three buckets:\n\n### Endpoints Accepting Either Token Type (`any`)\n\nThese endpoints work with both user tokens and client tokens.\n\n| Endpoint | Service | Notes |\n|----------|---------|-------|\n| `POST /v1/document/validate` | Document | Validate a template against the schema |\n| `POST /v1/document/upgrade` | Document | Migrate a template to a newer schema version |\n| `GET/POST /v1/publisher/jobs` | Publisher | Create/list publisher jobs |\n| `GET /v1/publisher/jobs/{...}` | Publisher | Job details, output files |\n| `GET/POST /v1/converter/jobs` | Converter | Create/list converter jobs |\n| `GET /v1/converter/jobs/{...}` | Converter | Job details, output files |\n| `GET /oauth/userinfo` | Identity | OpenID userinfo |\n| `GET /v1/file/organizations/{id}/teams` | File | The teams of one organization |\n| `GET /v1/file/teams/{id}/projects` | File | The projects of one team |\n| `GET /v1/file/projects/{id}` | File | One project |\n| `GET /v1/file/projects/{id}/templates` | File | The designs in a project |\n| `POST /v1/file/projects/{id}/templates/presigned-urls` | File | Thumbnail links for a page of designs — a read, despite the verb |\n| `POST /v1/file/projects/{id}/media/presigned-urls` | File | Read links for up to 100 media files — a read, despite the verb |\n| `POST /v1/file/projects/{id}/fonts/presigned-urls` | File | Read links for up to 100 font files — a read, despite the verb |\n| `GET /v1/file/templates/{id}` | File | One design |\n| `GET /v1/file/templates/{id}/{fileType}/download-url` | File | A short-lived link to read a design's thumbnail or document |\n| `GET /v1/file/templates/{id}/forms` | File | The ways one design can be filled in |\n| `GET /v1/file/templates/{id}/fields` | File | The merge fields of one design: what a row of data can fill |\n| `GET /v1/file/projects/{id}/forms` | File | The forms in a project |\n| `GET /v1/file/forms/{id}` | File | One form |\n| `GET /v1/subscription/me` | Subscription | The plan of the key's own organization (needs `subscription:read`) |\n| `GET /v1/subscription/plans`, `GET /v1/subscription/plans/{code}` | Subscription | The plan catalogue, the same for every organization (needs `subscription:read`) |\n\nThese file routes are the read side an integration needs — a webshop showing what it may sell.\nThe subscription route lets it check, when it is connected, that the organization's plan is one\nit can run on.\nA client token has no user and therefore no roles, so what it reaches is decided by the\n**organization** the token belongs to: a resource whose team sits in another organization\nanswers `403`, and every write stays closed to it.\n\n### Endpoints Requiring User Tokens\n\nEverything else under `/v1/file/*`, plus all of `/v1/webhook/*`, `/v1/subscription/*` (except `GET /v1/subscription/me` and the plan catalogue), and `/v1/identity/*`, requires a user token. This is because the request needs user context to resolve resource ownership, webhook ownership, subscription quota, and so on.\n\n| Service | Path | Notes |\n|---------|------|-------|\n| File | `/v1/file/*` (writes, and everything user-scoped) | Creating, editing, deleting, sharing, recents, favorites |\n| Webhook | `/v1/webhook/*` | **All** webhook endpoints — subscriptions, deliveries, retries, events list |\n| Subscription | `/v1/subscription/*` (except `GET /v1/subscription/me` and `/plans`) | Usage, add-ons |\n| Identity | `/v1/identity/users/me`, `/v1/identity/organizations/*`, `/v1/identity/oauth/scopes/me` | Profile, organizations, the scopes an organization's plan allows |\n\n:::info Why this split?\nUser-owned resources (files, webhook subscriptions, usage and add-ons) need user context to resolve ownership and quota. Reading the plan is the exception: `GET /v1/subscription/me` answers for the token's own organization, so an integration can check it with its app key, and the plan catalogue (`/v1/subscription/plans`) is the same for everyone. Stateless operations (document validate/upgrade, publisher and converter jobs) don't, so they accept either token type.\n:::","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#token-type-validation","section":"Token Type Validation","audience":"api","headings":["Error Response","HTTP Status"],"text":"The API validates token types at the gateway level. If you use the wrong token type, you'll receive a clear error message.\n\n### Error Response\n\n```json\n{\n  \"error\": \"invalid_token_type\",\n  \"error_description\": \"This endpoint requires a user token obtained via authorization_code flow. You provided a client token from client_credentials flow.\"\n}\n```\n\n### HTTP Status\n\nToken type errors return `403 Forbidden` with error code `invalid_token_type`.","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#identifying-token-type","section":"Identifying Token Type","audience":"api","headings":["Via Introspection","Via Token Response"],"text":"### Via Introspection\n\nUse the token introspection endpoint to check your token's type:\n\n```bash\ncurl -X POST https://api.typograph.nl/oauth/introspect \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=YOUR_ACCESS_TOKEN\" \\\n  -d \"client_id=YOUR_CLIENT_ID\" \\\n  -d \"client_secret=YOUR_CLIENT_SECRET\"\n```\n\nCheck these fields in the response:\n\n| Field | User Token | Client Token |\n|-------|------------|--------------|\n| `grant_type` | `authorization_code` or `refresh_token` | `client_credentials` |\n| `auth_type` | `user` | `client` |\n| `sub` | User UUID | Not present |\n\n### Via Token Response\n\nThe token response includes the scope, but doesn't directly indicate token type. Use introspection to verify.","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#refreshed-tokens","section":"Refreshed Tokens","audience":"api","headings":[],"text":"When you refresh a token using the `refresh_token` grant, the new token **preserves the original token type**:\n\n- Refreshing a user token produces another user token\n- The `sub` claim is preserved\n- The `grant_type` in introspection shows `refresh_token`\n- The `auth_type` remains `user`\n\n```json\n{\n  \"grant_type\": \"refresh_token\",\n  \"auth_type\": \"user\",\n  \"sub\": \"019b28fb-a11e-7641-a28f-e978f892ec07\"\n}\n```","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#common-scenarios","section":"Common Scenarios","audience":"api","headings":["Scenario 1: Web Application","Scenario 2: Backend Publisher / Converter Service","Scenario 3: Webhook Consumer","Scenario 4: Hybrid Application"],"text":"### Scenario 1: Web Application\n\nA web application where users log in and manage their templates.\n\n**Solution:** Use Authorization Code flow with PKCE to obtain user tokens.\n\n```javascript\n// User authenticates via OAuth\nconst userToken = await authenticateUser();\n\n// Access user's templates\nconst templates = await fetch('/v1/file/teams/my-team/templates', {\n  headers: { 'Authorization': `Bearer ${userToken}` }\n});\n```\n\n### Scenario 2: Backend Publisher / Converter Service\n\nA backend service that generates PDFs or converts files on a schedule.\n\n**Solution:** Client Credentials works — publisher and converter endpoints accept either token type.\n\n```javascript\nconst clientToken = await getClientToken();\n\n// Works — publisher accepts client tokens\nconst job = await fetch('https://api.typograph.nl/v1/publisher/jobs', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${clientToken}`,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    input: {\n      template: { url: 'https://example.com/template.json' },\n      manifest: { url: 'https://example.com/manifest.json' }\n    },\n    output: {\n      upload: { url: 'https://example.com/upload-target' },\n      config: { type: 'pdf' }\n    }\n  })\n});\n\n// Works — reading designs is open to an app key, within its own organization\nconst templates = await fetch(`https://api.typograph.nl/v1/file/projects/${projectId}/templates`, {\n  headers: { 'Authorization': `Bearer ${clientToken}` }\n});\n\n// Would FAIL — changing a design is a thing a person does\nawait fetch(`https://api.typograph.nl/v1/file/templates/${templateId}`, {\n  method: 'PATCH',\n  headers: { 'Authorization': `Bearer ${clientToken}` },\n  body: JSON.stringify({ name: 'Renamed' })\n});\n```\n\nFor anything that writes, you'll need to capture a user token (Authorization Code + PKCE) during a prior interactive flow and store the refresh token securely for background use. A sign-in lasts at most 30 days, however often you refresh it: plan for the user to authorize your app again by then ([Refresh Token Flow](https://docs.typograph.nl/guides/api/authentication#refresh-token-flow)).\n\n### Scenario 3: Webhook Consumer\n\nA service that manages its own webhook subscriptions.\n\n**Solution:** Use a **user token**. All `/v1/webhook/*` endpoints require user context because subscriptions are owned by a user within an organization.\n\n```javascript\nconst userToken = await getUserToken(); // from Authorization Code + PKCE\n\nconst subscription = await fetch('https://api.typograph.nl/v1/webhook/subscriptions', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${userToken}`,\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    name: 'Pipeline notifications',\n    client_id: '<your-oauth-client-uuid>',\n    endpoint: { url: 'https://your-service.com/webhook' },\n    event_types: ['publication.completed', 'conversion.completed']\n  })\n});\n```\n\n### Scenario 4: Hybrid Application\n\nA service that needs both user context and background processing.\n\n**Solution:** Use user tokens for user-initiated actions, store refresh tokens for background tasks.\n\n```javascript\n// User authenticates\nconst { accessToken, refreshToken } = await authenticateUser();\n\n// Store refresh token securely for later use\nawait secureStorage.store(userId, refreshToken);\n\n// Later, in a background job\nconst storedRefreshToken = await secureStorage.get(userId);\nconst newAccessToken = await refreshAccessToken(storedRefreshToken);\n\n// Now you have a user token for the background job\n```\n\nThe background job keeps working for at most 30 days after the user signed in: then the refresh answers `400 invalid_grant` with `\"reason\": \"sign_in_expired\"`, and the user authorizes your app again ([Refresh Token Flow](https://docs.typograph.nl/guides/api/authentication#refresh-token-flow)).","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#best-practices","section":"Best Practices","audience":"api","headings":[],"text":"1. **Choose the right flow** - Use Authorization Code for user-facing apps, Client Credentials for server-to-server only when accessing endpoints that accept client tokens.\n\n2. **Check endpoint requirements** - Before integrating, verify whether the endpoint requires a user token.\n\n3. **Handle errors gracefully** - If you receive `invalid_token_type`, check your authentication flow.\n\n4. **Store tokens securely** - For background processing with user context, securely store and refresh user tokens.\n\n5. **Use introspection** - When debugging, use token introspection to verify your token type.","agent_tools":[]},{"title":"Token Types","url":"https://docs.typograph.nl/guides/api/token-types#related-documentation","section":"Related Documentation","audience":"api","headings":[],"text":"- [Authentication](https://docs.typograph.nl/guides/api/authentication) - Complete OAuth 2.0 guide\n- [Scopes](https://docs.typograph.nl/guides/api/scopes) - API permission scopes\n- [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting) - Request limits and quotas","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers","section":null,"audience":"api","headings":[],"text":"A [publisher](https://docs.typograph.nl/guides/api/publisher) or [converter](https://docs.typograph.nl/guides/api/converter) job fetches files from URLs you give it and uploads the result to another one. When those URLs need a header, for example an `Authorization` token for a private CDN, put it in `url_headers`. This is one list of rules at the top level of the job, and every request the job makes to a URL a rule covers carries that rule's headers.\n\n```json\n{\n  \"url_headers\": [\n    {\n      \"match\": \"https://cdn.example.com/private/\",\n      \"headers\": { \"Authorization\": \"Bearer your-cdn-token\" }\n    },\n    {\n      \"match\": \"https://fonts.example.com/\",\n      \"headers\": { \"X-Api-Key\": \"your-font-key\" }\n    }\n  ],\n  \"input\": {\n    \"bundle\": { \"url\": \"https://cdn.example.com/private/orders/995/bundle.json\" }\n  },\n  \"output\": {\n    \"upload\": {\n      \"url\": \"https://pod.example.com/webhooks/typograph/995\",\n      \"headers\": { \"Authorization\": \"Basic dXNlcjpwYXNz\" }\n    },\n    \"config\": { \"type\": \"pdf\" }\n  }\n}\n```\n\nHere the bundle download gets the CDN token, every font under `https://fonts.example.com/` gets the font key, and the upload gets its own `Authorization` header.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#the-shape","section":"The shape","audience":"api","headings":[],"text":"`url_headers` is optional. It is a list of at most **10** rules, and each rule has exactly two fields:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `match` | string | The `https` URL, or URL prefix, the headers are sent to |\n| `headers` | object | Header names and values, at most **5** per rule |\n\nThe per-URL `headers` you already know (`input.template.headers`, `input.source.headers`, `output.upload.headers`, `output.config.watermark.image.headers` and so on) keep working. They behave like an exact rule for that one URL, with one difference: an empty per-URL `headers` object is the same as leaving it out, so the prefix rules still apply. A per-URL `headers` object cannot opt a URL out; only a rule in `url_headers` with an empty `headers` object can (see [below](https://docs.typograph.nl/guides/api/url-headers#presigned-urls-and-authorization-dont-mix)).","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#which-requests-a-rule-covers","section":"Which requests a rule covers","audience":"api","headings":["Prefix or exact","Which rule wins","Scheme, host and port are compared exactly"],"text":"Every outgoing request of the job:\n\n- **Publisher:** the downloads of `template`, `manifest`, `bundle` and `csv`, the check at `POST /jobs` that those URLs answer, the fonts and images the manifest names, the watermark image, and the upload. With `input.csv`, every child job the batch creates gets its own copy of the parent's rules for the requests it makes.\n- **Converter:** the download of `input.source` and the upload.\n\n### Prefix or exact\n\n- A `match` that **ends in `/`** is a **prefix**. It covers every URL whose path starts with it: `https://cdn.example.com/private/` covers `https://cdn.example.com/private/a.json` and `https://cdn.example.com/private/fonts/b.woff2`, whatever their query string.\n- Any other `match` is **exact**. It covers that one URL, query string included.\n\n### Which rule wins\n\nFor each request the headers come from one place only, in this order:\n\n1. the URL's own `headers` (the per-URL shorthand);\n2. otherwise an exact rule for that URL;\n3. otherwise the **longest** prefix that covers the URL;\n4. otherwise no headers.\n\nA URL's own `headers` replace an exact rule in `url_headers` with the same match. That is not an error: the duplicate check below compares the rules in `url_headers` with each other only.\n\nHeaders of different rules are never merged. A URL covered by both `https://cdn.example.com/` and `https://cdn.example.com/private/` gets only the headers of the second.\n\n### Scheme, host and port are compared exactly\n\nA rule only covers URLs with the same scheme, host and port: `https://example.com/` does not cover `https://cdn.example.com/` or `https://example.com:8443/`. There is no wildcard for subdomains. Give each host its own rule. Host names and schemes are compared case-insensitively, and the default port counts as given (`https://example.com/` equals `https://example.com:443/`). Paths are compared case-sensitively, after `.` and `..` segments are resolved.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#rules-for-a-valid-url_headers","section":"Rules for a valid url_headers","audience":"api","headings":[],"text":"A job that breaks one of these is refused with `422 Unprocessable Entity` and `\"error\": \"validation_error\"`:\n\n- **https only.** `match` is an absolute `https` URL with a host. `http` is refused, and so are user info (`user:pass@`) and a fragment (`#…`). A prefix match cannot carry a query string.\n- **No duplicates:** two rules cannot have the same `match`.\n- **Limits:** at most 10 rules, a `match` of at most 2000 characters (a presigned URL in an exact rule counts its query string), at most 5 headers per rule, and a header value of at most 8 KB (8192 bytes). A rule may have an empty `headers` object: the URLs that rule wins for get no headers at all. On an exact rule that opts one URL out; on a prefix rule it opts out every URL below it, unless a longer prefix or an exact rule covers that URL.\n- **No line breaks:** a header value cannot contain CR, LF or NUL, and a header name must be a valid HTTP token.\n- **Only allowed header names**, compared case-insensitively:\n\n| Allowed | Names |\n|---------|-------|\n| Authentication | `Authorization`, `X-Api-Key`, `X-Auth-Token`, `Api-Key` |\n| Content negotiation | `Accept`, `Accept-Encoding`, `Accept-Language`, `Content-Type`, `Content-Disposition` |\n| Request tracking | `X-Request-Id`, `X-Correlation-Id`, `X-Trace-Id` |\n| Caching | `Cache-Control`, `If-None-Match`, `If-Modified-Since`, `ETag` |\n| Custom | any other `X-…` name |\n\n`Host`, `Cookie`, `Origin`, `Referer`, the `Proxy-…` and connection headers, `Content-Length`, and `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Real-Ip` and `X-Powered-By` are always refused. On an upload, `Content-Type` and `User-Agent` are set by Typograph and cannot be overridden.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#presigned-urls-and-authorization-dont-mix","section":"Presigned URLs and Authorization don't mix","audience":"api","headings":[],"text":"S3, R2 and other object stores refuse a request that carries **both** a signature in the query string and an `Authorization` header (\"only one auth mechanism allowed\"). A broad prefix such as `https://bucket.s3.example.com/` with an `Authorization` header therefore breaks every presigned URL below it.\n\n- Keep prefixes **narrow**: cover the directory that needs the header, not the whole bucket.\n- Where one URL below a prefix must go without the header, add an exact rule for it with an empty `headers` object. It wins over the prefix, so that URL gets no headers at all:\n\n```json\n\"url_headers\": [\n  { \"match\": \"https://bucket.s3.example.com/\", \"headers\": { \"Authorization\": \"Bearer your-token\" } },\n  { \"match\": \"https://bucket.s3.example.com/exports/995.pdf?X-Amz-Signature=abc\", \"headers\": {} }\n]\n```\n\n  An exact rule matches the query string too, so give the presigned URL exactly as the job uses it. A URL's own non-empty `headers` also win over the prefix, and they replace its headers rather than adding to them.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#what-happens-to-the-headers","section":"What happens to the headers","audience":"api","headings":[],"text":"- **Never returned.** No response, not `POST /jobs` and not `GET /jobs/{id}`, contains `url_headers` or any `headers` value, and neither does any webhook event.\n- **Forgotten when the job ends.** Typograph keeps the rules only while the job runs and deletes them once it is `completed` or `failed`. In a CSV batch each child job keeps its own copy until that child ends, and the parent keeps its own until the last row has ended.\n- A header is sent only to URLs its rule covers. A prefix rule sends the secret to *every* URL below that prefix, so keep prefixes to hosts and paths you control.\n- **Redirects are not followed.** A download or upload that answers with a redirect fails the job, so a rule's headers never travel on to another host.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#limits-of-what-headers-reach","section":"Limits of what headers reach","audience":"api","headings":[],"text":"- **Images inside an SVG.** When an SVG image in the manifest refers to another image with `<image href=\"…\">`, that nested image is fetched without headers. Make it public, or give it a presigned URL.\n- **The editor and canvas in the browser can't use them.** `url_headers` applies to the server-side requests of a job only. The editor, canvas and viewer load fonts and images straight from the browser, so an asset that needs a header does not show there. For a template that is edited as well as published, use URLs the browser can load, such as presigned URLs.","agent_tools":[]},{"title":"Headers for the URLs of a job","url":"https://docs.typograph.nl/guides/api/url-headers#related","section":"Related","audience":"api","headings":[],"text":"- [Publisher Service](https://docs.typograph.nl/guides/api/publisher)\n- [Converter Service](https://docs.typograph.nl/guides/api/converter)","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks","section":null,"audience":"api","headings":[],"text":"Webhooks notify your application when events occur in Typograph. Instead of polling for status changes, configure webhook endpoints to receive real-time notifications.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#webhook-management-api","section":"Webhook Management API","audience":"api","headings":["List Subscriptions","Create Subscription","Get Subscription","Update Subscription","Rotate Secret","Delete Subscription","List Available Events"],"text":"Manage your webhook subscriptions programmatically using the Webhook Service API. An AI agent can do the same through the [MCP server's webhook tools](https://docs.typograph.nl/guides/api/mcp-server#webhooks).\n\n**Required scopes:** `webhook` (full access), `webhook:read`, `webhook:write`, `webhook:delete`.\n\n**Token type:** All `/v1/webhook/*` endpoints require a **user token** — subscriptions are owned by a user within an organization. Client Credentials tokens are rejected. See [Token Types](https://docs.typograph.nl/guides/api/token-types).\n\nA subscription or delivery `{id}` in a path that names nothing of your organization's, or is\nnot a UUID, answers `404 Not Found` (`not_found`).\n\n### List Subscriptions\n\n```bash\ncurl \"https://api.typograph.nl/v1/webhook/subscriptions?limit=20&offset=0\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nAdd `client_id=<uuid>` to list only the subscriptions of one OAuth client. A `limit` over 50, or a\n`client_id` that is not a UUID, is refused with `400 Bad Request` (`invalid_request`).\n\n**Response:** list entries are a summary view: they omit `organization_id` and `client_id`, which `GET /subscriptions/{id}` does return. No read ever includes the `secret`, and endpoint header *values* are masked (`***`) on every read; see [Create Subscription](https://docs.typograph.nl/guides/api/webhooks#create-subscription).\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n      \"name\": \"Production pipeline\",\n      \"endpoint\": {\n        \"url\": \"https://your-app.com/webhooks/typograph\",\n        \"headers\": { \"X-Tenant-Id\": \"***\" }\n      },\n      \"event_types\": [\"publication.completed\", \"conversion.completed\"],\n      \"is_active\": true,\n      \"failure_count\": 0,\n      \"last_failure_at\": null,\n      \"created_at\": \"<ISO 8601 timestamp>\",\n      \"updated_at\": \"<ISO 8601 timestamp>\"\n    }\n  ],\n  \"meta\": {\n    \"total\": 1,\n    \"limit\": 20,\n    \"offset\": 0,\n    \"has_more\": false\n  }\n}\n```\n\nSee [Pagination](https://docs.typograph.nl/guides/api/pagination) for the `limit` / `offset` contract.\n\n### Create Subscription\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/webhook/subscriptions \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Production pipeline\",\n    \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n    \"endpoint\": {\n      \"url\": \"https://your-app.com/webhooks/typograph\",\n      \"headers\": { \"X-Tenant-Id\": \"tenant-123\" }\n    },\n    \"event_types\": [\"publication.completed\", \"publication.failed\"]\n  }'\n```\n\n**Request fields:**\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `name` | string (≤ 255) | yes | Human-readable subscription name |\n| `client_id` | UUID v7 | yes | OAuth client of your organization the subscription belongs to; it cannot be changed later |\n| `endpoint.url` | URL (≤ 2000) | yes | HTTPS endpoint that will receive deliveries |\n| `endpoint.headers` | object | no | Up to 5 custom headers sent with every delivery |\n| `event_types` | array, ≥ 1 entry | yes | Event types to subscribe to (see [below](https://docs.typograph.nl/guides/api/webhooks#available-event-types)) |\n\n**Response** (`201 Created`): the full subscription, including `organization_id`, `client_id`, the raw `secret`, and the same secret as `standard_webhooks_secret` for the official Standard Webhooks libraries — **this is the only time the full secret is shown.** Store it now; it cannot be retrieved again. Every later read (`GET /subscriptions/{id}`, `GET /subscriptions`, `PATCH`) returns the subscription without either secret field, and masks `endpoint.headers` values as `***`. If you lose the secret, call [Rotate Secret](https://docs.typograph.nl/guides/api/webhooks#rotate-secret) to get a new one.\n\nThe `secret` is a raw string, used as the HMAC key exactly as it is. `standard_webhooks_secret` is the same key in the `whsec_` format of the official Standard Webhooks libraries: pass that one to a library. See [Secret Format](https://docs.typograph.nl/guides/api/webhooks#secret-format).\n\nA `client_id` that is not a client of your organization is refused with `403 Forbidden`\n(`access_denied`), the same whether the client belongs to another organization or does not exist.\nIf the client cannot be checked at that moment, the request is refused with\n`503 Service Unavailable`; send it again.\n\n### Get Subscription\n\n```bash\ncurl https://api.typograph.nl/v1/webhook/subscriptions/{id} \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\n### Update Subscription\n\n```bash\ncurl -X PATCH https://api.typograph.nl/v1/webhook/subscriptions/{id} \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Updated name\",\n    \"endpoint\": {\n      \"url\": \"https://your-app.com/webhooks/v2/typograph\",\n      \"headers\": { \"X-Tenant-Id\": \"tenant-123\" }\n    },\n    \"event_types\": [\"publication.completed\"],\n    \"is_active\": true\n  }'\n```\n\nAll PATCH fields are optional. Send only the fields you want to change.\n\nEndpoint header values come back masked as `***` on every read. A header you send with the value `***` keeps its stored value, so you can read a subscription, change one field and send the endpoint back without overwriting the real header values. Sending `***` for a header that has no stored value is a `400`. To remove every custom header, send the endpoint with an empty headers object: `\"endpoint\": { \"url\": \"https://your-app.com/webhooks/typograph\", \"headers\": {} }`.\n\n### Rotate Secret\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/webhook/subscriptions/{id}/rotate-secret \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\n**Response** (`200 OK`): the subscription with a freshly generated `secret` and its `standard_webhooks_secret` — again, the only time they are shown. Use this if a secret has leaked, or periodically as a best practice.\n\nFor 24 hours after a rotate, deliveries are signed with **both** the new and the previous secret: the `webhook-signature` header carries two space-separated `v1,<signature>` values. Verify against either one during that window (the helper in [Signature Verification](https://docs.typograph.nl/guides/api/webhooks#signature-verification) already loops over them), then drop the old secret once you have switched. After 24 hours only the new secret signs deliveries.\n\nA second rotate inside those 24 hours is refused with `409 Conflict` (`\"error\": \"rotation_in_progress\"`, with the time the overlap ends in `error_description`), so the secret your endpoint verifies with is never dropped while you are still switching.\n\n**A leaked secret:** rotate with `revoke_previous=true` to stop every earlier secret at once. This is accepted inside the 24-hour window too, so a secret that leaks right after a routine rotate can still be dropped immediately. There is no overlap: from then on only the new secret signs, so deliveries fail at your endpoint until it verifies with the new secret.\n\n```bash\ncurl -X POST \"https://api.typograph.nl/v1/webhook/subscriptions/{id}/rotate-secret?revoke_previous=true\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\n### Delete Subscription\n\n```bash\ncurl -X DELETE https://api.typograph.nl/v1/webhook/subscriptions/{id} \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nReturns `204 No Content` on success.\n\n### List Available Events\n\n```bash\ncurl https://api.typograph.nl/v1/webhook/events \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nReturns the current event-type catalog — use this as the authoritative list when validating user input in your own UI.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#available-event-types","section":"Available Event Types","audience":"api","headings":[],"text":"Only the event types emitted by the Typograph backend can be subscribed to. The current catalog is:\n\n| Event type | Trigger |\n|------------|---------|\n| `publication.completed` | A publisher (PDF/image generation) job finished successfully |\n| `publication.failed` | A publisher job failed |\n| `conversion.completed` | A converter (InDesign / font / image conversion) job finished successfully |\n| `conversion.failed` | A converter job failed |\n| `template.updated` | A template was renamed, moved, restored, or saved from the editor |\n| `template.deleted` | A template was moved to the trash |\n| `form.updated` | A form's fields, steps, linked templates or status changed |\n| `form.deleted` | A form was deleted, leaving its templates with no way to be filled in |\n| `project.deleted` | A project was moved to the trash, taking every design in it |\n| `subscription.upgraded` | The organization moved to a higher plan |\n| `subscription.downgraded` | The organization's move to a lower plan took effect |\n| `subscription.cancelled` | The organization's subscription was cancelled |\n| `subscription.reactivated` | A cancelled subscription was reactivated |\n\nThe job events reach the subscriptions of the client that started the job, within the\norganization the job ran for: another organization's subscription never receives them. The design events reach every\nsubscription of the **organization** that owns the design, because the change is usually made\nby a designer in the portal while the integration that needs to hear about it is somewhere\nelse entirely. The subscription events reach the organization whose plan changed, for the same\nreason.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#webhook-delivery","section":"Webhook Delivery","audience":"api","headings":["HTTP Request","Headers","Request Body","Success Criteria"],"text":"When an event fires, the webhook service `POST`s the event payload to `endpoint.url`.\n\n### HTTP Request\n\n| Property | Value |\n|----------|-------|\n| Method | `POST` |\n| Content-Type | `application/json` |\n| Timeout | 30 seconds |\n| TLS | Required (HTTPS endpoints only) |\n\n### Headers\n\nEvery delivery includes [Standard Webhooks](https://www.standardwebhooks.com/) signature headers:\n\n| Header | Description | Example |\n|--------|-------------|---------|\n| `webhook-id` | Unique delivery identifier (UUID) | `019b28fb-a11e-7641-a28f-e978f892ec06` |\n| `webhook-timestamp` | Unix epoch seconds at send time | `1704672000` |\n| `webhook-signature` | HMAC-SHA256 signature, format `v1,{base64}`; two space-separated values for 24 hours after a [secret rotation](https://docs.typograph.nl/guides/api/webhooks#rotate-secret) | `v1,K3Z8mHdf...` |\n| `webhook-event-type` | The event type of this delivery | `template.updated` |\n| `Content-Type` | `application/json` | |\n\nPlus any custom headers you configured on `endpoint.headers` (up to 5). The headers above are **protected** — custom headers with those names are ignored.\n\n### Request Body\n\nThe request body is the **raw event payload** — no envelope, no wrapper. The event type is not in the body: read it from the `webhook-event-type` header.\n\n#### `publication.completed` / `publication.failed`\n\nThe body is the publisher job snapshot plus identifiers, without the `headers` of any of its URLs:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"job_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"tag\": \"invoice-alpha\",\n  \"parent_id\": null,\n  \"status\": \"completed\",\n  \"message\": null,\n  \"error_code\": null,\n  \"page_count\": 4,\n  \"input\": { \"template\": { \"url\": \"...\" }, \"manifest\": { \"url\": \"...\" } },\n  \"output\": {\n    \"upload\": { \"url\": \"...\" },\n    \"config\": { \"type\": \"pdf\", \"single_pages\": false, \"rotate_pages\": [{ \"page\": 2, \"degrees\": 90 }] }\n  },\n  \"expires_at\": \"<ISO 8601 timestamp>\",\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"updated_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\nThe file is where you sent it: `output.upload.url`. When the job ended is `updated_at`. `output.config.rotate_pages` is the same `{page, degrees}` list the job was created with, `null` when it turns no page.\n\n`page_count` is the number of pages in the file the job delivered: the pages of a PDF, `1` for an image, and for a merge the pages of all its rows together. It is `null` for a multi-page image job, whose pages are jobs of their own that each send their own event with `page_count: 1`.\n\nFor `publication.failed`, the payload additionally includes an `error` field with the failure reason, `status: \"failed\"`, and `error_code`, a stable code for the reason, to act on or to show the failure in your own language. `message` and `error` are written for people and may change; the code does not. Show `error` for a code you do not know: more may be added.\n\n`page_count` and `error_code` are in the webhook payload only: [`GET /v1/publisher/jobs/{id}`](https://docs.typograph.nl/guides/api/publisher) does not return them, and the same holds for `error_code` and the converter's jobs.\n\n| `error_code` | The job failed because |\n|---|---|\n| `download_failed` | a file of the job (template, manifest, bundle or CSV) could not be downloaded |\n| `file_too_large` | a file of the job is larger than the limit |\n| `invalid_input` | the bundle or the template is not what a job takes: not JSON, or not a JSON object |\n| `invalid_merge_data` | the CSV of a merge cannot fill the template: unreadable, empty, or not matching its fields |\n| `row_limit_exceeded` | the CSV of a merge has more rows than your plan merges |\n| `plan_unavailable` | your plan could not be checked, so the merge was not started; try again |\n| `layout_unavailable` | the template had to be laid out again, and the layout service did not answer; try again |\n| `layout_failed` | the template had to be laid out again, and its pages could not be built |\n| `fonts_unavailable` | the template had to be laid out again, and its fonts did not all load |\n| `text_overflow` | laid out again, the text no longer fits its frames and would print cut off |\n| `generation_failed` | the PDF or image could not be generated |\n| `merge_row_failed` | a row of the merge did not print, so the merge delivers nothing; the `report` names the rows |\n| `upload_failed` | the result could not be uploaded to `output.upload.url` |\n| `stalled` | the job stopped in a step and did not continue |\n\nA job with a CSV sends one event, for the parent, once every row ended: its rows send none of their own. Its payload carries `row_count`, `printed_row_count`, `failed_row_count` and the `report` of the rows that did not print; see [Following a merge](https://docs.typograph.nl/guides/api/publisher#following-a-merge).\n\n#### `conversion.completed` / `conversion.failed`\n\nThe body is the converter job snapshot plus identifiers, without the `headers` of any of its URLs:\n\n```json\n{\n  \"id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"job_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"client_id\": \"019b28fb-a11e-7641-a28f-e978f892ec01\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"tag\": \"font-conversion-001\",\n  \"status\": \"completed\",\n  \"message\": null,\n  \"error_code\": null,\n  \"warnings\": [],\n  \"input\": { \"type\": \"font\", \"url\": \"...\" },\n  \"output\": { \"type\": \"woff2\", \"upload\": { \"url\": \"...\" } },\n  \"expires_at\": \"<ISO 8601 timestamp>\",\n  \"created_at\": \"<ISO 8601 timestamp>\",\n  \"updated_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\nFor `conversion.failed`, the payload includes an `error` field, `status: \"failed\"`, and `error_code`, a stable code for the reason (`null` in `conversion.completed`). As for a publication, act on the code and show `error` for one you do not know.\n\n| `error_code` | The job failed because |\n|---|---|\n| `download_failed` | the input file could not be downloaded |\n| `conversion_failed` | the input could not be converted |\n| `document_not_built` | the converted package could not be built into pages, so there is no template to deliver |\n| `upload_failed` | the result could not be uploaded to `output.upload.url` |\n| `stalled` | the job stopped in a step and did not continue |\n\nA `conversion.completed` for a package conversion can carry a `message`: the template was\nconverted, but its text was not laid out (a font missing from the package, for example). See\n[Text Layout](https://docs.typograph.nl/guides/api/converter#text-layout). `warnings` lists what the conversion could not\ncarry over, one `{code, message, ...}` per item, and is `[]` when there is nothing; see\n[Warnings](https://docs.typograph.nl/guides/api/converter#warnings).\n\nA package conversion whose converted template cannot be opened, so that no page can be built from\nit, fails rather than deliver a blank template: `conversion.failed`, with the reason in `message`\nand in `error`, and `error_code: \"document_not_built\"`. See [Text Layout](https://docs.typograph.nl/guides/api/converter#text-layout).\n\n#### `template.updated` / `template.deleted`\n\nEnough to find the design again and decide what to do; the design itself is fetched through the\nAPI as usual.\n\n```json\n{\n  \"template_id\": \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n  \"project_id\": \"019b28fb-a11e-7641-a28f-e978f892ec02\",\n  \"team_id\": \"019b28fb-a11e-7641-a28f-e978f892ec03\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"name\": \"Welkom\",\n  \"updated_at\": \"<ISO 8601 timestamp>\",\n  \"reason\": \"saved\"\n}\n```\n\n`reason` is `saved` when the editor wrote a new version, `renamed` for a metadata change,\n`restored` when a deleted template came back, and `moved` when it changed project. A `moved`\nevent also carries `previous_project_id`, because a move can put the design outside what your\napp key is allowed to read — which is a different problem from the design having changed.\n`template.deleted` carries the same body without `reason`.\n\nOpening the editor does not fire `template.updated` — only a write does.\n\n#### `form.updated` / `form.deleted`\n\n```json\n{\n  \"form_id\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n  \"project_id\": \"019b28fb-a11e-7641-a28f-e978f892ec02\",\n  \"team_id\": \"019b28fb-a11e-7641-a28f-e978f892ec03\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"name\": \"Personaliseren\",\n  \"status\": \"published\",\n  \"template_ids\": [\"019b28fb-a11e-7641-a28f-e978f892ec06\"],\n  \"updated_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\n`template_ids` names the designs this form fills in. That link is the one thing that can change\nwithout either the form or the design changing, so linking a form to a different template\narrives here as a `form.updated` whose `template_ids` moved — nothing else in the payload would\ngive it away.\n\n`status` is what decides whether the form can still be filled in at all: anything other than\n`published` means a visitor gets a form with nothing in it. `form.deleted` carries the same body,\nand means the same thing permanently.\n\n#### `project.deleted`\n\n```json\n{\n  \"project_id\": \"019b28fb-a11e-7641-a28f-e978f892ec02\",\n  \"team_id\": \"019b28fb-a11e-7641-a28f-e978f892ec03\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"name\": \"Kerst 2026\",\n  \"updated_at\": \"<ISO 8601 timestamp>\"\n}\n```\n\nA project in the trash takes every design in it, and **no `template.deleted` is emitted for any\nof them** — the project goes, not the templates one by one. If you keep references to designs,\nthis is the only word you get that a whole shelf of them just became unreadable.\n\nRestoring a project emits nothing. Neither does an app key being revoked, a scope being\nnarrowed, or a delivery being retried past the point where it is given up on — which is why an\nintegration that must not sell what it cannot make should reconcile on a schedule as well as\nlisten.\n\nA form going from `draft` to `published` arrives as an update like any other, so an integration\nthat only sells published forms can act on `status`.\n\n#### `subscription.upgraded`, `subscription.downgraded`, `subscription.cancelled`, `subscription.reactivated`\n\n```json\n{\n  \"subscription_id\": \"019b28fb-a11e-7641-a28f-e978f892ec05\",\n  \"organization_id\": \"019b28fb-a11e-7641-a28f-e978f892ec04\",\n  \"plan_code\": \"free\",\n  \"plan_name\": \"Free\",\n  \"status\": \"active\",\n  \"billing_cycle\": \"monthly\",\n  \"previous_plan_code\": \"pro\"\n}\n```\n\nFor an integration that needs a particular plan: a webshop that runs on Pro (`pro`) and up has\nto hear the moment an organization drops below it. `previous_plan_code` is present on upgrades\nand downgrades. The plan codes are `free`, `pro`, `enterprise` and `internal`.\n\n| Event | Sent when | `status` |\n|---|---|---|\n| `subscription.upgraded` | the new plan takes effect, at once | `active` |\n| `subscription.downgraded` | the lower plan takes effect, at the end of the period it was scheduled for | `active` |\n| `subscription.cancelled` | somebody cancels: the cancellation is **scheduled**, not done | `cancelled` |\n| `subscription.reactivated` | a cancelled subscription is taken back before it ends | `active` |\n\nA cancelled subscription keeps its plan until the end of the period it was paid for; nothing\nis sent when that period runs out. To know when access really ends, read `current_period_ends_at`\nfrom `GET /v1/subscription/me`.\n\nA downgrade is sent when it **takes effect** — at the end of the billing period it was\nscheduled for — not when somebody schedules it, because until then the organization still has\nthe plan it paid for. To read the plan itself, call `GET /v1/subscription/me` with a client token\nthat has `subscription:read`; the payload names a plan code, and what that code allows is the\nplan's to say.\n\nSee the [Publisher](https://docs.typograph.nl/guides/api/publisher) and [Converter](https://docs.typograph.nl/guides/api/converter) references for the complete job schemas.\n\n### Success Criteria\n\nA delivery is considered **successful** when your endpoint returns HTTP **200-299**. Anything else (3xx, 4xx, 5xx, network error, timeout) is a failure and triggers the retry schedule.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#retry-behavior","section":"Retry Behavior","audience":"api","headings":["Automatic Subscription Disable"],"text":"Failed deliveries retry with an exponential backoff:\n\n| Attempt | Delay | Total elapsed |\n|---------|-------|---------------|\n| 1 (initial) | immediate | 0 |\n| 2 | 1 min | 1 min |\n| 3 | 5 min | 6 min |\n| 4 | 15 min | 21 min |\n| 5 | 1 hr | 1 hr 21 min |\n| 6 | 2 hr | 3 hr 21 min |\n\nAfter 6 total attempts the delivery is marked **permanently failed**. Its [delivery history](https://docs.typograph.nl/guides/api/webhooks#delivery-history) shows the HTTP status your endpoint answered (`response_status`) and why the last attempt failed (`error_message`, `error_code`).\n\n### Automatic Subscription Disable\n\nA subscription is automatically disabled after **10 consecutive failed deliveries** across any events. When disabled, `is_active` flips to `false` and no further deliveries are attempted until you re-enable it:\n\n```bash\ncurl -X PATCH https://api.typograph.nl/v1/webhook/subscriptions/{id} \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"is_active\": true}'\n```\n\nA successful delivery resets the failure counter, and so does re-enabling a disabled subscription: it is disabled again only after another 10 consecutive failed deliveries.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#signature-verification","section":"Signature Verification","audience":"api","headings":["Secret Format","Signature Construction","Verification Steps","Node.js","PHP"],"text":"Always verify signatures before processing a webhook.\n\n:::note\nThe `secret` used here is the **per-subscription webhook signing secret** returned by `POST /subscriptions` and `POST /subscriptions/{id}/rotate-secret` — it is **not** your OAuth application/client secret. Each subscription has its own, and no later read ever returns it again.\n\nFor 24 hours after a rotate, `webhook-signature` may carry two space-separated `v1,<signature>` values (new secret first, previous secret second). Try each one in turn; see [Rotate Secret](https://docs.typograph.nl/guides/api/webhooks#rotate-secret).\n:::\n\n### Secret Format\n\nThe `secret` is a 64-character hex string, and **the string itself is the HMAC-SHA256 key**: sign with its characters as they are. Do not hex-decode it into 32 bytes first. The examples on this page do exactly that, so with them you need nothing else.\n\nThis is not the secret format of the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Its official `standardwebhooks` libraries expect `whsec_` followed by the base64 of the key bytes, and they base64-decode whatever comes after the prefix. A hex string is valid base64, so passing our `secret` to them as it is gives no error. It gives a different key, and every delivery fails with *No matching signature found*.\n\n#### With the official Standard Webhooks libraries\n\nCreate and rotate return the secret in their format as well, as `standard_webhooks_secret`: `whsec_` followed by the base64 of the secret string. It is the same key, so the signatures match. Pass it to the library as it is.\n\nFor a secret you stored without it, make it yourself:\n\n```\nlibrary_secret = \"whsec_\" + base64(secret)\n```\n\n| Language | Conversion |\n|----------|------------|\n| Node.js | `'whsec_' + Buffer.from(secret).toString('base64')` |\n| Python | `'whsec_' + base64.b64encode(secret.encode()).decode()` |\n| PHP | `'whsec_' . base64_encode($secret)` |\n\nEach library also takes the raw secret directly, if you say it is raw: `new Webhook(secret, { format: 'raw' })` in JavaScript, `Webhook(secret.encode())` in Python (bytes are used as the key as they are, a string is read as `whsec_`), and `Webhook::fromRaw($secret)` in PHP.\n\nThe examples below convert a stored `secret`. If you stored `standard_webhooks_secret`, pass it to `new Webhook(...)` as it is instead.\n\n```javascript\nconst { Webhook } = require('standardwebhooks');\n\nconst wh = new Webhook('whsec_' + Buffer.from(process.env.TYPOGRAPH_WEBHOOK_SECRET).toString('base64'));\n\n// Throws when the signature or the timestamp does not check out.\nconst payload = wh.verify(rawBody, {\n  'webhook-id': req.headers['webhook-id'],\n  'webhook-timestamp': req.headers['webhook-timestamp'],\n  'webhook-signature': req.headers['webhook-signature'],\n});\n```\n\n```python\nimport base64, os\nfrom standardwebhooks import Webhook\n\nsecret = os.environ['TYPOGRAPH_WEBHOOK_SECRET']\nwh = Webhook('whsec_' + base64.b64encode(secret.encode()).decode())\n\n# Raises WebhookVerificationError when the signature or the timestamp does not check out.\npayload = wh.verify(raw_body, {\n    'webhook-id': headers['webhook-id'],\n    'webhook-timestamp': headers['webhook-timestamp'],\n    'webhook-signature': headers['webhook-signature'],\n})\n```\n\n```php\nuse StandardWebhooks\\Webhook;\n\n$wh = new Webhook('whsec_' . base64_encode($webhookSecret));\n\n// Throws WebhookVerificationException when the signature or the timestamp does not check out.\n$payload = $wh->verify($rawBody, [\n    'webhook-id' => $_SERVER['HTTP_WEBHOOK_ID'],\n    'webhook-timestamp' => $_SERVER['HTTP_WEBHOOK_TIMESTAMP'],\n    'webhook-signature' => $_SERVER['HTTP_WEBHOOK_SIGNATURE'],\n]);\n```\n\nThe libraries check the 5-minute timestamp window themselves. During a rotation's overlap they accept either of the two signatures, so the [rotation](https://docs.typograph.nl/guides/api/webhooks#rotate-secret) needs nothing extra from you. Pass the raw request body, not a re-encoded JSON object: re-encoding changes the bytes that were signed.\n\n### Signature Construction\n\n```\nmessage   = \"{webhook-id}.{webhook-timestamp}.{raw-body}\"\nsignature = base64(hmac_sha256(message, secret))   # key = the secret string as is\nheader    = \"v1,\" + signature\n```\n\n### Verification Steps\n\n1. Extract `webhook-id`, `webhook-timestamp`, and `webhook-signature` from the request headers.\n2. Reject if `|now - webhook-timestamp| > 300` seconds (prevents replay).\n3. Recompute the signature using your stored `secret`.\n4. Compare against each space-separated value in `webhook-signature` using a timing-safe comparison — there may be two during a rotation's overlap window.\n\n### Node.js\n\n```javascript\nconst crypto = require('crypto');\nconst TOLERANCE_SECONDS = 300;\n\nfunction verifyWebhookSignature(webhookId, timestamp, rawBody, signatureHeader, secret) {\n  const now = Math.floor(Date.now() / 1000);\n  if (Math.abs(now - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) {\n    return false;\n  }\n\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(`${webhookId}.${timestamp}.${rawBody}`)\n    .digest('base64');\n  const expectedBuf = Buffer.from(expected);\n\n  // signatureHeader may carry two space-separated \"v1,<sig>\" values during a\n  // secret rotation's 24-hour overlap window; accept either one.\n  return signatureHeader.split(' ').some((entry) => {\n    const received = entry.startsWith('v1,') ? entry.slice(3) : entry;\n    const receivedBuf = Buffer.from(received);\n    return receivedBuf.length === expectedBuf.length && crypto.timingSafeEqual(receivedBuf, expectedBuf);\n  });\n}\n\napp.post('/webhooks/typograph', express.raw({ type: 'application/json' }), (req, res) => {\n  const ok = verifyWebhookSignature(\n    req.headers['webhook-id'],\n    req.headers['webhook-timestamp'],\n    req.body.toString(),\n    req.headers['webhook-signature'],\n    WEBHOOK_SECRET,\n  );\n\n  if (!ok) {\n    return res.status(401).json({ error: 'Invalid signature' });\n  }\n\n  const payload = JSON.parse(req.body);\n  // payload.job_id, payload.status, etc. — process the event\n\n  res.status(200).json({ received: true });\n});\n```\n\n### PHP\n\n```php\n<?php\n\nconst TOLERANCE_SECONDS = 300;\n\nfunction verifyWebhookSignature(\n    string $webhookId,\n    string $timestamp,\n    string $rawBody,\n    string $signatureHeader,\n    string $secret,\n): bool {\n    if (abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {\n        return false;\n    }\n\n    $expected = base64_encode(hash_hmac('sha256', \"{$webhookId}.{$timestamp}.{$rawBody}\", $secret, true));\n\n    // $signatureHeader may carry two space-separated \"v1,<sig>\" values during a\n    // secret rotation's 24-hour overlap window; accept either one.\n    foreach (explode(' ', $signatureHeader) as $entry) {\n        $received = str_starts_with($entry, 'v1,') ? substr($entry, 3) : $entry;\n        if (hash_equals($expected, $received)) {\n            return true;\n        }\n    }\n\n    return false;\n}\n\n$rawBody   = file_get_contents('php://input');\n$webhookId = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';\n$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';\n$signature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';\n\nif (!verifyWebhookSignature($webhookId, $timestamp, $rawBody, $signature, $webhookSecret)) {\n    http_response_code(401);\n    exit('Invalid signature');\n}\n\n$payload = json_decode($rawBody, true);\n// ... process $payload\nhttp_response_code(200);\n```","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#delivery-history","section":"Delivery History","audience":"api","headings":["Retry a Failed Delivery"],"text":"View past deliveries for a subscription:\n\n```bash\ncurl \"https://api.typograph.nl/v1/webhook/subscriptions/{id}/deliveries?limit=20&offset=0\" \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nEach delivery entry includes its status, the event it delivered, how many times it was attempted (`attempt_count`), the HTTP status your endpoint answered (`response_status`), why the last attempt failed, and when the next attempt is due. Use [Pagination](https://docs.typograph.nl/guides/api/pagination) for paging.\n\nA failed attempt carries `error_message`, a short English phrase, and `error_code`, a stable code to show the failure in your own language. Show `error_message` for a code you do not know: more may be added.\n\n| `error_code` | The last attempt failed because |\n|---|---|\n| `http_status` | your endpoint answered outside 2xx; the status is in `response_status` |\n| `connection_failed` | no connection could be made to your endpoint |\n| `timeout` | your endpoint did not answer within 30 seconds |\n| `tls_error` | the TLS handshake or the certificate check failed |\n| `blocked_address` | your endpoint resolves to an address that is not allowed, such as a private network |\n| `https_required` | the endpoint URL does not use https |\n| `subscription_inactive` | the subscription was switched off |\n| `delivery_failed` | any other failure |\n\n### Retry a Failed Delivery\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/webhook/deliveries/{id}/retry \\\n  -H \"Authorization: Bearer YOUR_ACCESS_TOKEN\"\n```\n\nThis forces a new attempt immediately, bypassing the normal exponential-backoff schedule, and answers the delivery with status `retrying`. A delivery's status is `pending`, `delivered`, `failed` or `retrying`. Any delivery that has not arrived (`pending`, `failed` or `retrying`) can be retried; one that is already `delivered` is refused with `400` (`invalid_request`, \"Cannot retry a delivered webhook\"). Each retry is a new request to your endpoint, so one delivery is retried at most once a minute: another retry of it within that minute is refused with `429` (`rate_limit_exceeded`), and the `Retry-After` header says how many seconds to wait.","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#complete-handler-example-nodejs","section":"Complete Handler Example (Node.js)","audience":"api","headings":[],"text":"```javascript\nconst express = require('express');\nconst crypto = require('crypto');\n\nconst app = express();\nconst WEBHOOK_SECRET = process.env.TYPOGRAPH_WEBHOOK_SECRET;\nconst TOLERANCE_SECONDS = 300;\nconst processed = new Set();\n\nfunction verify(webhookId, timestamp, rawBody, sigHeader, secret) {\n  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > TOLERANCE_SECONDS) return false;\n  const expected = Buffer.from(crypto.createHmac('sha256', secret)\n    .update(`${webhookId}.${timestamp}.${rawBody}`)\n    .digest('base64'));\n  // Two space-separated \"v1,<sig>\" values for 24 hours after a secret rotation.\n  return sigHeader.split(' ').some((entry) => {\n    const received = Buffer.from(entry.startsWith('v1,') ? entry.slice(3) : entry);\n    return received.length === expected.length && crypto.timingSafeEqual(received, expected);\n  });\n}\n\napp.post('/webhooks/typograph',\n  express.raw({ type: 'application/json' }),\n  async (req, res) => {\n    const webhookId = req.headers['webhook-id'];\n    const ok = verify(\n      webhookId,\n      req.headers['webhook-timestamp'],\n      req.body.toString(),\n      req.headers['webhook-signature'],\n      WEBHOOK_SECRET,\n    );\n    if (!ok) return res.status(401).json({ error: 'Invalid signature' });\n\n    // Respond immediately, process async (must respond within 30 s)\n    res.status(200).json({ received: true });\n\n    if (processed.has(webhookId)) return;\n    processed.add(webhookId);\n\n    const payload = JSON.parse(req.body);\n    if (payload.status === 'completed') {\n      // payload.job_id available; fetch the generated file\n      const fileUrl = `https://api.typograph.nl/v1/publisher/jobs/${payload.job_id}/files?file_type=output`;\n      await downloadAndProcess(fileUrl);\n    } else if (payload.status === 'failed') {\n      console.error(`Job ${payload.job_id} failed: ${payload.error}`);\n    }\n  },\n);\n\napp.listen(3000);\n```","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#best-practices","section":"Best Practices","audience":"api","headings":[],"text":"- **Respond within 30 seconds.** ACK with `200` immediately, process asynchronously.\n- **Verify every request.** Signature + timestamp window (5 min). Reject unverified payloads.\n- **Deduplicate on `webhook-id`.** Retries may re-deliver the same event.\n- **Keep handlers idempotent.** Same payload processed twice must produce the same result.\n- **Monitor `failure_count`.** Subscriptions auto-disable after 10 consecutive failures.\n- **Rotate your secret periodically** with `POST /subscriptions/{id}/rotate-secret`, and always keep it out of source control. Verify against both signatures during the 24-hour overlap (see [Rotate Secret](https://docs.typograph.nl/guides/api/webhooks#rotate-secret)).","agent_tools":[]},{"title":"Webhooks","url":"https://docs.typograph.nl/guides/api/webhooks#direct-upload-alternative","section":"Direct Upload Alternative","audience":"api","headings":[],"text":"Instead of (or in addition to) webhooks, publisher and converter jobs can upload the generated file directly to a URL you control by setting `output.upload.url` on the job request:\n\n```json\n{\n  \"output\": {\n    \"upload\": {\n      \"url\": \"https://your-server.com/uploads/\",\n      \"headers\": { \"Authorization\": \"Bearer your-upload-token\" }\n    },\n    \"config\": { \"type\": \"pdf\" }\n  }\n}\n```\n\nTypograph `PUT`s the generated bytes to that URL once the job completes, which is convenient for delivery straight to cloud storage (S3, GCS, Azure Blob). You can still subscribe to `publication.completed` / `conversion.completed` for the job-status signal. See [Publisher](https://docs.typograph.nl/guides/api/publisher) and [Converter](https://docs.typograph.nl/guides/api/converter) for the full job schema.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents","section":null,"audience":"api","headings":[],"text":"A Typograph **document** (also called a *template* when saved in the file service) is the versioned JSON structure that the editor reads, the validator checks, and the publisher renders. This page covers the top-level shape; the remaining pages in this section zoom in on pages, elements, and swatches.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#top-level-shape","section":"Top-Level Shape","audience":"api","headings":[],"text":"```json\n{\n  \"version\": \"1.0.0\",\n  \"type\": \"template\",\n  \"metadata\": { ... },\n  \"font_data\": [ ... ],\n  \"swatches\": [ ... ],\n  \"styles\": { ... },\n  \"spreads\": [ ... ],\n  \"active_spread_index\": 0,\n  \"user_data\": null\n}\n```\n\nEvery key Typograph defines is snake_case, as in all Typograph JSON. Keys taken from an external format keep their own names: the font descriptions inside `font_data` follow the Google Fonts API (see [Font Data](https://docs.typograph.nl/guides/concepts/documents#font-data)).\n\nDocuments saved with the older key names are still read (see [Older key names](https://docs.typograph.nl/guides/concepts/documents#older-key-names)).\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `version` | string | yes | Schema version — must match the Typograph schema, currently `1.0.0` |\n| `type` | string | yes | Either `template` (a reusable design) or `clip` (a fragment) |\n| `metadata` | object | yes | `name`, `author`, `created_at` (when the document was first saved; kept on every later save) and `updated_at` |\n| `font_data` | array | yes | Fonts referenced by the document's text elements |\n| `swatches` | array of Swatch | yes | Color swatches used for fills, strokes, shadows, gradients. At least 4 required. |\n| `spreads` | array of Spread | yes | The spreads in the document, each holding one or more pages and the elements on them; see [Pages](https://docs.typograph.nl/guides/concepts/pages) |\n| `styles` | object | no | Paragraph, character, table and cell styles of rich text |\n| `stories` | array | no | Text stories that rich-text elements refer to |\n| `roles` | array | no | The edit-permission roles built for this document |\n| `masters` | array of Spread | no | Master spreads the document's pages apply |\n| `active_spread_index` | number | no | Spread to open first in the editor |\n| `facing_pages` | boolean | no | Whether the pages are shown as facing pairs |\n| `first_page_left` | boolean | no | Whether page 1 is a left-hand page, so pages 1 and 2 form the first pair |\n| `user_data` | object | no | Free-form data of your own; Typograph stores it and does not read it |\n| `brand_colors` | object | deprecated | Legacy alias for `swatches` — new documents should use `swatches` |\n\nUse the [Document Service](https://docs.typograph.nl/guides/api/document) (`POST /v1/document/validate`) to confirm a payload conforms to the schema before sending it to the publisher.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#type-template-vs-clip","section":"Type: template vs clip","audience":"api","headings":[],"text":"- `template` — a complete, renderable document. This is what the editor opens, what the publisher turns into PDFs/images, and what the file service stores under `/v1/file/templates`.\n- `clip` — a reusable fragment (e.g. a logo block, a call-out) that can be dropped into a template.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#swatch-based-color-system","section":"Swatch-Based Color System","audience":"api","headings":[],"text":"Typograph does not embed raw colors in individual elements. Instead, the document defines a palette of **swatches** (at the document level), and elements refer to them by UUID for fills, strokes, shadows, and gradients. This lets a color change in one place propagate throughout the document. See [Styles](https://docs.typograph.nl/guides/concepts/styles) for the swatch model.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#font-data","section":"Font Data","audience":"api","headings":[],"text":"`font_data` is the authoritative list of fonts the document uses. Each entry has an `index`, a `source`, an `is_active` flag, a `uuid`, and a `data` object describing the font family and its files. The `data` object follows the shape of the Google Fonts API, so its keys keep that API's names, such as `lastModified`. The editor fetches font files through the file service's font presigned-URL endpoints — see [File Service](https://docs.typograph.nl/guides/api/file-service).","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#schema-versioning-and-upgrade","section":"Schema Versioning and Upgrade","audience":"api","headings":[],"text":"Schema versions are semver-tagged and incompatible across major versions. The [Document Service](https://docs.typograph.nl/guides/api/document) exposes:\n\n- `POST /v1/document/validate` — check whether a payload conforms to the current schema\n- `POST /v1/document/upgrade` — migrate a payload from an older schema to the current version\n\nAlways validate a document before sending it to the publisher — the publisher rejects invalid documents outright.","agent_tools":[]},{"title":"Documents","url":"https://docs.typograph.nl/guides/concepts/documents#older-key-names","section":"Older key names","audience":"api","headings":[],"text":"The keys of the document were brought in line with one naming convention: a timestamp ends in `_at`, a\nboolean that states something starts with `is_` or `has_`, a reference into the document ends in\n`_uuid`, and nothing is abbreviated. A document or package saved with the older names keeps working:\nthe editor, the canvas, the form widget, the document, publisher and converter services and the file\nservice read each older key as the current one, and write only the current names. The Document\nService's `/upgrade` returns a document in the current names. Write new documents in the current\nnames; the older ones are read, not documented further.\n\n| Where | Older name | Current name |\n|-------|------------|--------------|\n| Document | `meta_data` (`creation_date`, `modification_date`) | `metadata` (`created_at`, `updated_at`) |\n| `font_data[]` | `active`; `data.isVariable`, `data.instanceCoordinates`, `data.modification_date` | `is_active`; `data.is_variable`, `data.instance_coordinates`, `data.lastModified` |\n| `layout_fonts[]` | `uuid` | `font_uuid` |\n| `brand_colors.colors[]` | `rgba` | `hex` |\n| Swatches | `rgb.R/G/B`, `cmyk.C/M/Y/K`, `lab.L`; `inklist`; `tint_value`; gradient `samples[].pos` | `rgb.r/g/b`, `cmyk.c/m/y/k`, `lab.l`; `ink_list`; `tint`; `samples[].position` |\n| Text properties | `marker_char`, `list_start_at`, `underline_color`, `strikethrough_color`, `fill_gradient_*`, `stroke_gradient_*` | `marker_character`, `list_start_number`, `underline_swatch_uuid`, `strikethrough_swatch_uuid`, `gradient_fill_*`, `gradient_stroke_*` |\n| Story tables and table styles | `alt_fill_a_uuid`, `alt_col_fill_a_uuid`, `alt_first_count`, `alt_col_next_count`, `alt_fill_a_tint`, … | `alternating_row_fill_a_swatch_uuid`, `alternating_column_fill_a_swatch_uuid`, `alternating_row_first_count`, `alternating_column_next_count`, `alternating_row_fill_a_tint`, … |\n| Spreads | `page_name`, `page_active`, `locked`, `cover`, `fill_tint_value`, `stroke_tint_value` | `name`, `is_active`, `is_locked`, `is_cover`, `fill_tint`, `stroke_tint` |\n| Pages | `fill_tint_value`; guides `visible` | `fill_tint`; guides `is_visible` |\n| Elements | `id`, `hidden`, `locked`, `shadow`, `over_flow` (`previous_uuid`, `next_uuid`), `native_rotation_cx/cy`, `border_size`, `native_border_size`, `fill_tint_value`, `stroke_tint_value`, `layer_info.open` | `index`, `is_hidden`, `is_locked`, `has_shadow`, `overflow` (`previous_element_uuid`, `next_element_uuid`), `native_rotation_center_x/y`, `stroke_width`, `native_stroke_width`, `fill_tint`, `stroke_tint`, `layer_info.is_open` |\n| Element layout and limits | `auto_layout.column_gab`, `row_gab`, `margin_left/right/top/bottom`; `auto_size.page_height_native_offset`, `auto_size.listeners[].uuid`; `edit_constraints.move.dx/dy`, `edit_constraints.resize.aspect_lock` | `column_gap`, `row_gap`, `left_margin`, `right_margin`, `top_margin`, `bottom_margin`; `native_page_height_offset`, `element_uuid`; `horizontal`, `vertical`, `lock_aspect_ratio` |\n| Shapes and images | type `circel`; `native_radiuses`; line `p1_native`, `p2_native`; `save_paths`, `save_path`; image `key`; asset sizes `w`, `h`; asset `web_url` | `circle`; `native_radii`; `native_start_point`, `native_end_point`; `saved_paths`, `saved_path`; `image_id`; `width`, `height`; `editor_url` |\n| Rich text | `story_data`, `tag_list`, `native_autoleading`, `native_margin_button`, `native_columns_cap`, `native_lang`, `native_min_chars_before/after`, `pod_tekst_style`, `native_text_stroke_size`, `gradient_text_fill_*`, `gradient_text_stroke_*`, `text_fill_tint_value`, `text_stroke_tint_value` | `story`, `tags`, `native_auto_leading`, `native_margin_bottom`, `native_column_gap`, `native_language`, `native_min_characters_before/after`, `pod_text_style`, `native_text_stroke_width`, `native_gradient_text_fill_*`, `native_gradient_text_stroke_*`, `text_fill_tint`, `text_stroke_tint` |\n| Rich text layout | `top_ly`, `col_edges`; cells `col`, `col_span`, `cell_x/y/w/h`; words `line_break` | `top_y`, `column_edges`; `column`, `column_span`, `x/y/width/height`; `is_line_break` |\n| Package | `templatebundle.json` | `template_bundle.json` |\n\nA few older keys are read and dropped, because the current document no longer has them:\n`rotation_center_set`, a rounded rectangle's `TL`, `TR`, `BL` and `BR` (its radii are `native_radii`),\nand a text override's `any_property`.","agent_tools":[]},{"title":"Elements","url":"https://docs.typograph.nl/guides/concepts/elements","section":null,"audience":"api","headings":[],"text":"A **page element** is anything that appears on a page — a text block, an image, a shape, a table, a group. All elements share a common set of baseline properties (identity, geometry, fill, stroke, shadow, opacity, blend mode). The `type` field on every element determines which additional, type-specific fields are present.","agent_tools":[]},{"title":"Elements","url":"https://docs.typograph.nl/guides/concepts/elements#element-types","section":"Element Types","audience":"api","headings":[],"text":"There are 10 element types:\n\n| `type` | Description |\n|--------|-------------|\n| `group` | A container grouping other elements |\n| `circle` | Circle / ellipse |\n| `roundedrect` | Rectangle with optional corner radius |\n| `triangle` | Triangle |\n| `line` | Line segment |\n| `image` | Raster image |\n| `table` | Tabular layout |\n| `rich` | Rich-text block with inline formatting |\n| `path` | Arbitrary SVG-style path |\n| `poly` | Polygon |\n\n:::note Older value `circel`\nOlder documents may carry the misspelled `circel` as their circle type. It is still read as `circle`, and written as `circle` (see [Older key names](https://docs.typograph.nl/guides/concepts/documents#older-key-names)).\n:::","agent_tools":[]},{"title":"Elements","url":"https://docs.typograph.nl/guides/concepts/elements#shared-properties","section":"Shared Properties","audience":"api","headings":["Identity & Naming","Geometry","Fill, Stroke & Gradient","Shadows","Blend & Opacity","Behavior & State"],"text":"Every element carries this baseline (abbreviated list):\n\n### Identity & Naming\n- `index` (numeric, a counter within the spread), `uuid` (UUID, what a reference to the element names), `name`\n\n### Geometry\n- `native_rect` — authoritative bounds, in mm\n- `angle` — rotation\n- `native_rotation_center_x`, `native_rotation_center_y` — rotation center\n- `flip_horizontal`, `flip_vertical` — mirroring\n\n### Fill, Stroke & Gradient\nElements reference swatches the same way spreads do:\n\n- `fill_swatch_uuid`, `fill_tint` — solid fill\n- `stroke_swatch_uuid`, `stroke_tint`, `native_stroke_width`, `stroke_dash` — border\n- `native_gradient_fill_start`, `native_gradient_fill_length`, `gradient_fill_angle` — gradient fill\n- `native_gradient_stroke_start`, `native_gradient_stroke_length`, `gradient_stroke_angle` — gradient stroke\n\n### Shadows\n- `has_shadow` (bool), `shadow_swatch_uuid`, `shadow_angle`\n- `native_shadow_length`, `native_shadow_offset_x`, `native_shadow_offset_y`, `native_shadow_blur`\n\n### Blend & Opacity\n- `opacity` (0–1)\n- `blend_mode` — one of 26 blend modes (see below)\n\n### Behavior & State\n- `is_locked`, `is_hidden`\n- `overflow` — for text that runs on from one frame into the next: `previous_element_uuid`, `next_element_uuid`\n- `auto_layout` — auto-layout container configuration (for groups that behave as layouts)\n- `auto_size` — auto-resizing rules\n- `text_wrap` — text in frames below this element flows around it\n- `layer_info` — the element's place in the editor's layer panel\n- `edit_options`, `edit_constraints` — what a customer may change on the element, and within which limits. Present only when the document restricts the element.\n- `user_data` — free-form data of your own; Typograph stores it and does not read it","agent_tools":[]},{"title":"Elements","url":"https://docs.typograph.nl/guides/concepts/elements#blend-modes","section":"Blend Modes","audience":"api","headings":[],"text":"The `blend_mode` field accepts 26 values mirroring the canvas compositing operations:\n\n```\ncolor, color-burn, color-dodge, copy, darken, destination-atop, destination-in,\ndestination-out, destination-over, difference, exclusion, hard-light, hue, lighten,\nlighter, luminosity, multiply, overlay, saturation, screen, soft-light,\nsource-atop, source-in, source-out, source-over, xor\n```\n\nAny other value is rejected by the validator.","agent_tools":[]},{"title":"Elements","url":"https://docs.typograph.nl/guides/concepts/elements#type-specific-fields","section":"Type-Specific Fields","audience":"api","headings":[],"text":"Each subclass adds its own fields — for example:\n\n- `image` elements hold `image_id`, the `id` of the image in the manifest, plus crop, scale, and color-correction fields\n- `rich` elements hold their text as a `story`, font settings (`font_name`, `font_size`, `font_style`, `default_font`), and their `tags`\n- `table` elements hold row/column structure and per-cell style\n\nFor the exact field set of each element type, send a sample payload to `POST /v1/document/validate` — the returned violations pinpoint any missing or misnamed fields.","agent_tools":[]},{"title":"Pages","url":"https://docs.typograph.nl/guides/concepts/pages","section":null,"audience":"api","headings":[],"text":"A Typograph document lays its pages out in **spreads**. A spread is the container: it holds one or more **page records** side by side, and every **element** on those pages, in one coordinate space. A single page is a spread with one page record; a facing pair is a spread with two. See [Elements](https://docs.typograph.nl/guides/concepts/elements) for element-level detail.\n\n```json\n{\n  \"spreads\": [\n    {\n      \"uuid\": \"0141645a-d2e7-4fa8-94a0-6d89be96b900\",\n      \"name\": \"Front\",\n      \"allow_page_shuffle\": true,\n      \"is_active\": true,\n      \"is_locked\": false,\n      \"bleed\": 3,\n      \"fill_swatch_uuid\": \"PAPER\",\n      \"fill_tint\": 1,\n      \"stroke_swatch_uuid\": \"BLACK\",\n      \"stroke_tint\": 1,\n      \"pages\": [\n        {\n          \"x\": 0, \"y\": 0, \"width\": 210, \"height\": 297,\n          \"left_margin\": 10, \"top_margin\": 10, \"right_margin\": 10, \"bottom_margin\": 10,\n          \"name\": \"\"\n        }\n      ],\n      \"elements\": [ ... ]\n    }\n  ]\n}\n```","agent_tools":[]},{"title":"Pages","url":"https://docs.typograph.nl/guides/concepts/pages#spreads","section":"Spreads","audience":"api","headings":["Identity and Behaviour","Bleed","Fill, Stroke & Gradient"],"text":"### Identity and Behaviour\n\n| Property | Description |\n|----------|-------------|\n| `uuid` | UUID, stable across edits |\n| `name` | User-visible name shown in the editor: the name of the spread's first page |\n| `is_active` | Whether this spread is part of the active export range |\n| `is_locked` | Whether the spread is locked in the editor |\n| `allow_page_shuffle` | When `false`, the spread's pages stay together and the page bar does not reshuffle them |\n| `is_cover` | `true` when the spread is a cover wrap: back cover, spine and front cover as three page records. A cover sits outside the document's page count. |\n| `spine_direction` | Which way the spine text of a cover runs: `top_to_bottom` (default) or `bottom_to_top` |\n| `user_data` | Free-form data of your own; Typograph stores it and does not read it |\n\n### Bleed\n\n| Property | Description |\n|----------|-------------|\n| `bleed` | Print bleed extending beyond the trim edge, in mm |\n| `bleed_left`, `bleed_top`, `bleed_right`, `bleed_bottom` | Bleed per side, when the sides differ |\n\n### Fill, Stroke & Gradient\n\nSpreads reference swatches the same way elements do (see [Styles](https://docs.typograph.nl/guides/concepts/styles)):\n\n- `fill_swatch_uuid`, `fill_tint` — solid fill\n- `stroke_swatch_uuid`, `stroke_tint` — border stroke\n- `native_gradient_fill_start`, `native_gradient_fill_length`, `gradient_fill_angle` — gradient fill\n- `native_gradient_stroke_start`, `native_gradient_stroke_length`, `gradient_stroke_angle` — gradient stroke","agent_tools":[]},{"title":"Pages","url":"https://docs.typograph.nl/guides/concepts/pages#page-records","section":"Page Records","audience":"api","headings":["Layout Aids"],"text":"Each entry in a spread's `pages` array is one page: its bounds and position in the spread, its margins, and its layout aids. A spread holds at least one.\n\n| Property | Description |\n|----------|-------------|\n| `x`, `y` | Position of the page within the spread (top-left origin), in mm |\n| `width`, `height` | Page dimensions, in mm |\n| `left_margin`, `top_margin`, `right_margin`, `bottom_margin` | Content safe-area margins, stored as four independent fields |\n| `name` | Page label within the spread (`\"\"` when unnamed) |\n| `fill_swatch_uuid`, `fill_tint` | The page's own background colour. Absent: the page takes its spread's colour. |\n| `user_data` | The page's own free-form data. Absent: its spread's `user_data` applies. |\n\n### Layout Aids\n\nPurely editorial — they don't render on output but help during editing:\n\n- `guides` — user-drawn ruler guides on the page, each with `is_visible`\n- `columns` — the column layout\n- `grid` — layout grids","agent_tools":[]},{"title":"Pages","url":"https://docs.typograph.nl/guides/concepts/pages#elements","section":"Elements","audience":"api","headings":[],"text":"The actual content lives in the spread's `elements` array — text blocks, images, shapes, tables, groups and so on — positioned in the spread's coordinate space. See [Elements](https://docs.typograph.nl/guides/concepts/elements) for the element model.","agent_tools":[]},{"title":"Styles, Swatches & Fonts","url":"https://docs.typograph.nl/guides/concepts/styles","section":null,"audience":"api","headings":[],"text":"Typograph does not use a traditional \"paragraph/character/object styles\" system where styles are defined globally and applied by name. Instead, visual style is expressed two ways:\n\n1. **Swatches** — color values defined once at the document level, referenced by UUID from pages and elements.\n2. **Per-element formatting** — rich-text elements carry their own font, size, and inline tag runs; geometric properties (stroke width, shadow, opacity, blend mode) live directly on each element.","agent_tools":[]},{"title":"Styles, Swatches & Fonts","url":"https://docs.typograph.nl/guides/concepts/styles#swatches","section":"Swatches","audience":"api","headings":["Using a Swatch","Gradient Swatches"],"text":"A swatch is a named color definition stored in the document's `swatches` array:\n\n| Property | Description |\n|----------|-------------|\n| `type` | `SWATCH`, `TINTSWATCH`, or `GRADIENT` |\n| `uuid` | Stable identifier used by pages and elements |\n| `name` | Human-readable name |\n| `model` | `PROCESS`, `SPOT`, `REGISTRATION`, `MIXEDINKMODEL`, `GRADIENT`, or `NONE` |\n| `space` | The color space the swatch is defined in: `CMYK`, `RGB`, `LAB`, `MIXEDINK`, or `NONE` |\n| `rgb`, `cmyk`, `lab` | Color values in one or more color spaces, with lower-case channels: `rgb.r`, `rgb.g`, `rgb.b`; `cmyk.c`, `cmyk.m`, `cmyk.y`, `cmyk.k`; `lab.l`, `lab.a`, `lab.b` |\n| `ink_list` | The inks of a mixed-ink swatch |\n| `tint` | Default tint value (0–1) |\n\nA document must contain at least 4 swatches.\n\n### Using a Swatch\n\nPages and elements don't embed raw colors — they reference swatches:\n\n```\n\"fill_swatch_uuid\":   \"019b28fb-a11e-7641-a28f-e978f892ec06\",\n\"fill_tint\":          0.8,\n\"stroke_swatch_uuid\": \"019b28fb-a11e-7641-a28f-e978f892ec07\",\n\"stroke_tint\":        1.0,\n\"shadow_swatch_uuid\": \"019b28fb-a11e-7641-a28f-e978f892ec08\"\n```\n\nChanging the swatch definition propagates to every page and element that references it by UUID.\n\n### Gradient Swatches\n\nGradient swatches have `type: GRADIENT` and carry their stops in a `gradient` object: its `samples`, each with a `position`, a `swatch_uuid` and an `opacity`. Elements apply them via `gradient_fill_angle`, `native_gradient_fill_start` and `native_gradient_fill_length`, and the matching `gradient_stroke_angle`, `native_gradient_stroke_start` and `native_gradient_stroke_length`.","agent_tools":[]},{"title":"Styles, Swatches & Fonts","url":"https://docs.typograph.nl/guides/concepts/styles#fonts","section":"Fonts","audience":"api","headings":[],"text":"Fonts used in the document are declared in the top-level `font_data` array. Each entry names a font and points at a font asset resolved through the File Service (see [File Service](https://docs.typograph.nl/guides/api/file-service) — project fonts with presigned URLs).\n\nText elements (`type: rich`) reference fonts by name:\n\n| Property | Description |\n|----------|-------------|\n| `font_name` | Name of a font declared in `font_data` |\n| `font_size` | Size in native units |\n| `font_style` | Regular, italic, weight, etc. |\n| `default_font` | Fallback if the named font isn't available |\n\nInline formatting (runs of bold, italic, or differently-styled text) uses a `Tag`-based model embedded in the rich-text content.","agent_tools":[]},{"title":"Styles, Swatches & Fonts","url":"https://docs.typograph.nl/guides/concepts/styles#element-level-visual-properties","section":"Element-Level Visual Properties","audience":"api","headings":[],"text":"Properties normally thought of as \"object styles\" live on each element directly, not in a shared style sheet:\n\n- Border: `native_stroke_width`, `stroke_dash`\n- Shadow: `has_shadow`, `shadow_angle`, `native_shadow_offset_x`, `native_shadow_offset_y`, `native_shadow_blur`\n- Opacity: `opacity` (0–1)\n- Blending: `blend_mode` — one of 26 values, see [Elements → Blend Modes](https://docs.typograph.nl/guides/concepts/elements#blend-modes)\n- Rotation: `angle`, `native_rotation_center_x`, `native_rotation_center_y`\n\nThese don't refer to a global style definition; they're set per element. Shared look-and-feel comes from using shared swatches.","agent_tools":[]},{"title":"Styles, Swatches & Fonts","url":"https://docs.typograph.nl/guides/concepts/styles#why-this-model","section":"Why This Model?","audience":"api","headings":[],"text":"The editor is optimized for interactive, direct manipulation — designers tweak an element and see the change immediately. Color consistency is maintained through the swatch palette; typography through `font_data`. This keeps the document portable (no external stylesheet to ship alongside) while still allowing documents-wide color and font changes to propagate through a single edit.","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration","section":null,"audience":"api","headings":[],"text":"The Editor and Viewer are CDN-hosted browser scripts. Each is constructed with a container and a small options object, and then configured through host providers passed at construction and DOM events on a `window.typograph` global. This page covers the most common patterns; the full walkthrough lives on the dedicated integration pages.","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration#editor","section":"Editor","audience":"api","headings":["Lifecycle events","Opening a document programmatically"],"text":"```html\n<script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n<div id=\"app\" style=\"position:absolute; inset:0;\"></div>\n\n<script type=\"module\">\n  import { Editor } from 'https://cdn.typograph.nl/editor/latest/editor.js';\n\n  const editor = new Editor({\n    container: '#app',\n    host: {\n      // Providers that supply content into the editor\n      documents: listDocuments,\n      templates: listTemplates,\n      clips: listClips,\n      // Manifest for the Images tab: images and fonts (swatches go through set_manifest_data)\n      manifest_data: { images, fonts },\n\n      // Providers that persist edits back to your backend\n      save_document: saveDocument,   // (name, data) => Promise<void>\n      save_template: saveTemplate,\n      save_clip: saveClip,           // (name, data, thumbnail) => Promise<void>\n    },\n    legacy_php_fallback: false,\n  });\n</script>\n```\n\nA provider may return its value directly or as a promise. With `legacy_php_fallback: false`,\ncapabilities you don't provide are hidden from the editor UI; with the fallback on, they go to\n`php/*.php` instead. The older `set_*_callback` setters are deprecated.\n\n### Lifecycle events\n\n| Event | Meaning |\n|-------|---------|\n| `typograph_editor_ready` | Editor finished setting up — add late providers via `editor.get_document().host.set_providers()` |\n| `typograph_document_opened` | A document has been opened in the editor |\n| `typograph_user_fonts_loaded` | User-supplied fonts from the manifest finished loading |\n| `typograph_pageelement_selected` | The user selected a page element (`event.detail.uuid`, `event.detail.message`) |\n\n### Opening a document programmatically\n\nThe document exists once the editor has booted, so open it from the ready event:\n\n```javascript\ndocument.addEventListener('typograph_editor_ready', () => {\n  window.typograph.get_typograph_document().open_document_url('https://.../template.json');\n});\n```\n\nThe global returns the same document as `editor.get_document()`.\n\nSee [Editor Integration](https://docs.typograph.nl/guides/integration/editor) for the complete provider signatures and a working example.","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration#viewer","section":"Viewer","audience":"api","headings":[],"text":"```html\n<script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n<div id=\"app\" style=\"position:absolute; inset:0;\"></div>\n\n<script type=\"module\">\n  import { Viewer } from 'https://cdn.typograph.nl/editor/latest/viewer.js';\n  new Viewer({ container: '#app' });\n</script>\n```\n\nThe Viewer is read-only and does not authenticate with Typograph directly — fetch the template JSON server-side (using a user token) and hand it to the page.\n\nWhat the user may change is decided by the template, not by the Viewer: with no authored `edit_options`, text editing and image content edits are allowed while frame move/resize/rotate are denied. See [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer).","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration#container-sizing","section":"Container Sizing","audience":"api","headings":[],"text":"Both take a `container` — a CSS selector, a bare element id, or an `HTMLElement`. Constrain it with CSS so it has a usable width and height (absolute/fixed positioning, or explicit `width`/`height`); the layout resolves against that box, so a container with no height gives you no height. Omit `container` and the Editor mounts full-page on `document.body`.","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration#authentication","section":"Authentication","audience":"api","headings":[],"text":"Neither the Editor nor the Viewer calls the Typograph API directly. Your backend:\n\n1. Holds OAuth credentials.\n2. Exchanges them for a **user token** (Authorization Code + PKCE) — never embed a `client_secret` in browser code.\n3. Uses the token to read/write templates via the [File Service](https://docs.typograph.nl/guides/api/file-service).\n4. Returns data to the page through your own endpoints, which the Editor's host providers invoke.\n\nSee [Authentication](https://docs.typograph.nl/guides/api/authentication).","agent_tools":[]},{"title":"Configuration","url":"https://docs.typograph.nl/guides/getting-started/configuration#next-steps","section":"Next Steps","audience":"api","headings":[],"text":"- [Quick Start](https://docs.typograph.nl/guides/getting-started/quick-start) — end-to-end OAuth + API call\n- [Editor Integration](https://docs.typograph.nl/guides/integration/editor) — host providers, manifest, save flow\n- [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer) — read-only template filling","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation","section":null,"audience":"api","headings":[],"text":"There are two ways to integrate with Typograph, and most real integrations combine them:\n\n1. **REST API** (via any HTTP client of your choice) — for server-to-server work, automation, and non-browser consumers.\n2. **Browser scripts** (Editor and Viewer, loaded from the CDN) — for embedding the document editor or the read-only template-filling UI in your own web app.","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation#prerequisites","section":"Prerequisites","audience":"api","headings":[],"text":"- **An OAuth client.** Register one in the [Typograph Portal](https://portal.typograph.nl) — you'll receive a `client_id` and (for confidential clients) a `client_secret`. See [Authentication → Getting Credentials](https://docs.typograph.nl/guides/api/authentication#getting-credentials) for the exact portal navigation.\n- **The scopes your integration needs.** See the [Scopes reference](https://docs.typograph.nl/guides/api/scopes).","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation#rest-api","section":"REST API","audience":"api","headings":[],"text":"Typograph's REST API speaks standard HTTP + JSON. You can use any HTTP client — `fetch` in the browser, `axios`/`got` in Node.js, `requests` in Python, `cURL`, Postman, etc.\n\n```bash\n# Obtain an access token (Client Credentials flow)\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=$TYPOGRAPH_CLIENT_ID\" \\\n  -d \"client_secret=$TYPOGRAPH_CLIENT_SECRET\" \\\n  -d \"scope=document publisher converter\"\n```\n\nSee the [API Overview](https://docs.typograph.nl/guides/api/overview) for the base URL, request format, error envelope, and rate-limit headers.","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation#browser-scripts","section":"Browser Scripts","audience":"api","headings":["Browser Security Note"],"text":"The Editor and Viewer are CDN-hosted scripts — include them directly in your HTML. There is no npm install step.\n\n```html\n<!-- The colour engine is required by both, and is loaded as a plain script -->\n<script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n\n<!-- Full editing experience -->\n<script type=\"module\" src=\"https://cdn.typograph.nl/editor/latest/editor.js\"></script>\n\n<!-- Read-only template filling -->\n<script type=\"module\" src=\"https://cdn.typograph.nl/editor/latest/viewer.js\"></script>\n```\n\nLoad one or the other, not both. Each is constructed with a container:\n\n```javascript\nimport { Editor } from 'https://cdn.typograph.nl/editor/latest/editor.js';\nnew Editor({ container: '#app' });\n```\n\nBoth expose a `window.typograph` global once they finish loading; you communicate with them through DOM events (`typograph_editor_ready`, `typograph_init_ready`, …) and the host providers you pass at construction. See the full walkthroughs:\n\n- [Editor Integration](https://docs.typograph.nl/guides/integration/editor)\n- [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer)\n\n### Browser Security Note\n\nThe Editor and Viewer talk to your page, not directly to the API — your backend is responsible for fetching template JSON with a user token and providing it to the scripts via host providers. Never embed a `client_secret` in a browser bundle.","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation#which-token-do-you-need","section":"Which Token Do You Need?","audience":"api","headings":[],"text":"Most REST endpoints require a **user token** (obtained via Authorization Code + PKCE). A smaller set accepts **client tokens** (obtained via Client Credentials) — publisher jobs, converter jobs, and document validation. The full matrix lives in [Token Types](https://docs.typograph.nl/guides/api/token-types).","agent_tools":[]},{"title":"Installation","url":"https://docs.typograph.nl/guides/getting-started/installation#next-steps","section":"Next Steps","audience":"api","headings":[],"text":"- [Quick Start](https://docs.typograph.nl/guides/getting-started/quick-start) — obtain a token and make your first API call end-to-end\n- [Configuration](https://docs.typograph.nl/guides/getting-started/configuration) — configure the browser Editor\n- [API Overview](https://docs.typograph.nl/guides/api/overview) — base URL, request headers, error format","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start","section":null,"audience":"api","headings":[],"text":"This walkthrough takes you from \"I have a `client_id`/`client_secret`\" to a successful API call in a few minutes. It uses the Client Credentials flow, which is appropriate for server-to-server work. If you're building a user-facing app, use [Authorization Code with PKCE](https://docs.typograph.nl/guides/api/authentication#authorization-code-flow-with-pkce) instead.","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start#1-obtain-an-access-token","section":"1. Obtain an Access Token","audience":"api","headings":[],"text":"```bash\ncurl -X POST https://api.typograph.nl/oauth/token \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"grant_type=client_credentials\" \\\n  -d \"client_id=$TYPOGRAPH_CLIENT_ID\" \\\n  -d \"client_secret=$TYPOGRAPH_CLIENT_SECRET\" \\\n  -d \"scope=document publisher converter webhook\"\n```\n\nResponse:\n\n```json\n{\n  \"access_token\": \"typ_abc123...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 172800,\n  \"scope\": \"document publisher converter webhook\"\n}\n```\n\nAccess tokens are valid for 2 days. Cache them rather than minting a new one per request.","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start#2-call-an-endpoint-that-accepts-client-tokens","section":"2. Call an Endpoint That Accepts Client Tokens","audience":"api","headings":[],"text":"Validate a document payload — requires the `document` or `document:read` scope, accepts a client token:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/document/validate \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @template.json\n```\n\nOr create a publisher job to generate a PDF. The publisher downloads your template from the `input.template.url` you provide and, once rendered, POSTs the result to `output.upload.url`:\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": {\n      \"template\": { \"url\": \"https://example.com/my-template.json\" },\n      \"manifest\": { \"url\": \"https://example.com/my-manifest.json\" }\n    },\n    \"output\": {\n      \"upload\": { \"url\": \"https://example.com/upload-target\" },\n      \"config\": { \"type\": \"pdf\" }\n    }\n  }'\n```\n\nPublisher and converter endpoints return a job with a `queued` status. See [Asynchronous Processing](https://docs.typograph.nl/guides/api/overview#asynchronous-processing).","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start#3-track-the-job","section":"3. Track the Job","audience":"api","headings":[],"text":"You have two options:\n\n- **Poll** `GET /v1/publisher/jobs/{id}` until `status` is `completed` or `failed`.\n- **Subscribe to a webhook** (recommended) — see [Webhooks](https://docs.typograph.nl/guides/api/webhooks). The service delivers `publication.completed` / `publication.failed` (and `conversion.completed` / `conversion.failed` for converter jobs) to your configured URL.\n\nOnce the job is `completed`, fetch the generated file directly from the jobs endpoint:\n\n```bash\ncurl \"https://api.typograph.nl/v1/publisher/jobs/{id}/files?file_type=output\" \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" -L\n```\n\nThe `file_type` query parameter accepts `output`, `template`, `manifest`, or `csv`.\n\nGenerated files are available for **4 days** after job completion, then return `410 Gone`.","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start#4-embed-the-editor-or-viewer-optional","section":"4. Embed the Editor or Viewer (Optional)","audience":"api","headings":[],"text":"For a browser integration, include the Editor or Viewer from the CDN and pass it the host providers your backend supports:\n\n```html\n<script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n<div id=\"app\" style=\"position:absolute; inset:0;\"></div>\n<script type=\"module\">\n  import { Editor } from 'https://cdn.typograph.nl/editor/latest/editor.js';\n\n  new Editor({\n    container: '#app',\n    host: {\n      // Return [{ name, url }, …] — the templates the user can start from.\n      templates: () => fetch('/api/templates').then(r => r.json()),\n      // Persist `data` (serialized template JSON) to your backend.\n      save_template: (name, data) => fetch('/api/templates', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ name, data }),\n      }),\n    },\n    legacy_php_fallback: false,\n  });\n</script>\n```\n\nSee [Editor Integration](https://docs.typograph.nl/guides/integration/editor) for every provider, and [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer) for the read-only template-filling UI.","agent_tools":[]},{"title":"Quick Start","url":"https://docs.typograph.nl/guides/getting-started/quick-start#next-steps","section":"Next Steps","audience":"api","headings":[],"text":"- [Authentication](https://docs.typograph.nl/guides/api/authentication) — Authorization Code with PKCE, refresh tokens, SSO\n- [Token Types](https://docs.typograph.nl/guides/api/token-types) — which endpoints accept client vs. user tokens\n- [Scopes](https://docs.typograph.nl/guides/api/scopes) — request the minimum scope set for your use case\n- [Core Concepts → Documents](https://docs.typograph.nl/guides/concepts/documents) — the document schema the API validates against","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor","section":null,"audience":"api","headings":[],"text":"The Typograph **Editor** is a full browser-based document editor, delivered as a CDN-hosted script. It builds its own chrome — menu bar, toolbar, content bar, parameter bar — and exposes a `window.typograph` global for wiring up loading, saving, and the image/font manifest.\n\nThere is no npm package — include the script from the CDN.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#quick-start","section":"Quick Start","audience":"api","headings":[],"text":"Two scripts, and no markup to author.\n\n```html\n<!DOCTYPE html>\n<html translate=\"no\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Typograph Editor</title>\n  <script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n</head>\n<body>\n  <div id=\"app\" style=\"position:absolute; top:0; left:0; right:0; bottom:0;\"></div>\n\n  <script type=\"module\">\n    import { Editor } from 'https://cdn.typograph.nl/editor/latest/editor.js';\n    const editor = new Editor({ container: '#app' });\n  </script>\n</body>\n</html>\n```\n\nThe colour engine (`jsColorEngineWeb.js`) is a bare-global script and stays your responsibility. Everything else — the chrome, the stylesheet, the webfont — the bundle brings itself.\n\n`Editor` is also exposed as `window.Typograph.Editor` if you would rather not use a module script.\n\n:::info Upgrading from an older integration\nEarlier versions of these docs required the page to reproduce ~31 specific element ids (`wrapper`, `dialoglayer`, `dialoglayer_01`, `popupsliderlayer`, `htmllayer`, `menulayer`, …). **That is no longer necessary.** The bundle builds its own DOM.\n\nExisting pages keep working: the bundle detects that markup and leaves it alone, and a page that links its own `stylesheets/style.css` or Source Sans Pro keeps using its own copy — including a customised stylesheet. Nothing needs to change on your side, but new integrations should not author that markup.\n:::","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#full-page-vs-contained","section":"Full-page vs. contained","audience":"api","headings":["container","The container must have a height","Everything stays inside the container","Construct it early"],"text":"Load the two scripts and construct nothing, and the editor mounts itself on `document.body`, filling the viewport. Every existing host relies on this, so it is not going away.\n\nConstruct an `Editor` with a `container` and the chrome is built inside that box instead of taking over the page.\n\n### `container`\n\nA CSS selector, a bare element id, or an `HTMLElement` — all three work:\n\n```javascript\nnew Editor({ container: '#app' });                          // selector\nnew Editor({ container: 'app' });                           // bare id\nnew Editor({ container: document.getElementById('app') });  // element\n```\n\nIf nothing matches, a warning is logged and the editor mounts on `document.body` rather than failing silently.\n\n### The container must have a height\n\n**This is the one requirement.** The editor's layout is absolutely positioned and resolves against the container's box, so a container with no height gives you an editor with no height. A plain `<div id=\"app\"></div>` collapses to zero.\n\n```css\n#app { position: absolute; top: 44px; left: 24px; right: 24px; bottom: 24px; }\n```\n\n```css\n#app { height: 90vh; }\n```\n\n```css\n/* Inside a flex or grid parent that gives it a track. */\n#app { min-height: 0; }\n```\n\nAn unpositioned container is given `position: relative`, and one with visible overflow is given `overflow: hidden` so nothing escapes the box you allotted. Both are restored on `destroy()`.\n\n### Everything stays inside the container\n\nMenus, dialogs, colour pickers and the floating tool palettes are all confined to the box you gave the editor — none of them can be drawn over the rest of your page. A dialog that would be centred on screen in the full-page editor is centred in your container instead.\n\nThe one consequence worth knowing: a container much smaller than the editor's natural layout clips its own overlays rather than letting them escape. The chrome assumes roughly the room a desktop application gets.\n\n### Construct it early\n\nConstruct the `Editor` **before the window load event**. Later than that and the editor has already mounted itself on `document.body` under the full-page defaults; `container` is then ignored and a warning says so. A module script in the page body is early enough.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#options","section":"Options","audience":"api","headings":["The language"],"text":"| Option | Type | Default | What it does |\n|--------|------|---------|--------------|\n| `container` | selector \\| id \\| `HTMLElement` | `document.body` | Where the chrome is built. |\n| `header_space` | `boolean` \\| `number` | `57` (px) | The empty strip above the menu bar, for a host's logo. |\n| `host` | object of providers | none | Lists, manifest and save calls the editor uses — see [Host Providers](https://docs.typograph.nl/guides/integration/editor#host-providers). |\n| `legacy_php_fallback` | `boolean` | `true` on `latest` | `false`: no `php/*.php` request, ever, and UI without a provider is hidden. |\n| `locale` | `'en'` \\| `'nl'` | `data-typograph-locale` on `<html>`, else `'en'` | The language of the editor's own texts and dates. `'nl-NL'` counts as `'nl'`; another language falls back to English. |\n\nThe editor reserves 57px above the menu bar for a host logo. If your application already has its own header, you will not want it:\n\n```javascript\nnew Editor({ container: '#app', header_space: false });   // remove it\nnew Editor({ container: '#app', header_space: 80 });      // or set your own height\n```\n\nEverything below moves with it. Omit the option and the layout is exactly what it has always been.\n\n### The language\n\nThe editor shows its menus, toolbar, panels, dialogs and comments in English or Dutch, and writes its dates in that language, in Europe/Amsterdam time. Pass `locale`, or, for a page that lets the editor mount itself, set the language on `<html>` before `editor.js` loads:\n\n```javascript\nnew Editor({ container: '#app', locale: 'nl' });\n```\n\n```html\n<html data-typograph-locale=\"nl\">\n```\n\nThe option wins over the attribute. Without either the editor is English, as it has always been. The document's own text, font and style names, and the names of elements and layers are never translated, and menu items keep the English ids you address them by. `window.gEditorUI.get_locale()` returns the language in force.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#script-url","section":"Script URL","audience":"api","headings":[],"text":"| Channel | URL |\n|---------|-----|\n| Latest stable | `https://cdn.typograph.nl/editor/latest/editor.js` |\n| Minified | `https://cdn.typograph.nl/editor/latest/editor.min.js` |\n| Colour engine | `https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js` |\n\nLoad `editor.js` as a **module** (`<script type=\"module\" src=\"...\">`).","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#lifecycle","section":"Lifecycle","audience":"api","headings":[],"text":"```javascript\nconst editor = new Editor({ container: '#app' });\n\neditor.get_document();   // the canvas document, once booted\neditor.destroy();        // remove the editor's DOM and styles\n```\n\nRe-mounting after `destroy()` is not supported — reload the page instead. Constructing a second `Editor` destroys the first; two editors side by side is not supported.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#lifecycle-events","section":"Lifecycle Events","audience":"api","headings":[],"text":"The editor emits document events you listen to with `document.addEventListener(...)`:\n\n| Event | When it fires | Handler receives |\n|-------|---------------|------------------|\n| `typograph_init_ready` | Canvas is up and `window.typograph` is populated. | — |\n| `typograph_editor_ready` | The editor has finished setting up. Providers you add late go on `get_document().host` here. | — |\n| `typograph_document_opened` | A document has been opened. | — |\n| `typograph_user_fonts_loaded` | User-provided fonts (from the manifest) finished loading. | — |\n| `typograph_pageelement_selected` | The user selected a page element. | `CustomEvent` with `detail.uuid` and `detail.message` |\n\nSee [Document Events](https://docs.typograph.nl/editor/latest/documents/DocumentEvents.html) and [Element Events](https://docs.typograph.nl/editor/latest/documents/ElementEvents.html) for the full set.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#host-providers","section":"Host Providers","audience":"api","headings":["Missing providers hide their UI"],"text":"The editor has no backend of its own. Everything it needs from outside the open document goes\nthrough **host providers**: listing documents, templates and clips, the image/font manifest, and\nsaving. You pass them once, as `host`, when you construct the editor:\n\n```javascript\nconst editor = new Editor({\n  container: '#app',\n  host: {\n    // Lists — return an array (or a promise of one) of { name, url } objects.\n    documents: () => getJson('/api/documents'),\n    templates: () => getJson('/api/templates'),\n    clips:     () => getJson('/api/clips'),\n\n    // The manifest: pass it directly if you have it at page load (push)…\n    manifest_data: { images, fonts },\n    // …or supply `manifest: () => getJson('/api/manifest')` to load it lazily (pull). Not both.\n\n    // Saving — `data` is the serialized document JSON string.\n    save_document: (name, data) => postForm('/api/documents', { name, data }),\n    save_template: (name, data) => postForm('/api/templates', { name, data }),\n    save_clip:     (name, data, thumbnail) => postForm('/api/clips', { name, data, thumbnail }),\n  },\n  legacy_php_fallback: false,\n});\n\nasync function getJson(url) {\n  const response = await fetch(url);\n  return response.json();\n}\n\nasync function postForm(url, fields) {\n  const form = new FormData();\n  Object.entries(fields).forEach(([key, value]) => value !== undefined && form.append(key, value));\n  await fetch(url, { method: 'POST', body: form });\n}\n```\n\nEvery provider may return its value directly or as a promise, and the editor always awaits it.\nProviders you only know after startup can be added later with\n`editor.get_document().host.set_providers({ … })`.\n\n### Missing providers hide their UI\n\nWith `legacy_php_fallback: false`, a capability you don't provide simply isn't offered. Leave out\n`save_template` and *Save as template* disappears; leave out `clips` and the Clips tab\ndisappears. *Save As* (download to a local file) and *Open local* always stay. The full table is\nin [EditorUI API → Capability-driven UI](https://docs.typograph.nl/editor/latest/documents/API-EditorUI.html).\n\n:::info `legacy_php_fallback`\nOlder editors fell back to relative `php/*.php` endpoints on your host when a callback was missing.\nThat still happens on `latest` unless you pass `legacy_php_fallback: false`, and each fallback logs\na `[TYP-697]` deprecation warning. New integrations should always turn it off. Version 2 removes it.\n:::\n\n:::caution Deprecated: `set_*_callback`\n`set_documents_callback`, `set_templates_callback`, `set_clips_callback`, `set_save_document_callback`,\n`set_save_template_callback` and `set_save_clip_callback` still work on `latest`, but they are\ndeprecated, log a warning, and are removed in version 2. The [form widget](https://docs.typograph.nl/guides/integration/form) has\nno provider path yet. Its own `widget.set_save_template_callback` is not deprecated; registering on\nthe canvas document instead, as its quick start does, logs this warning.\nEach has a provider of the same name, but\nthe shapes differ, so adapt the function rather than passing it unchanged:\n\n| Setter callback | Provider |\n|--------|------|\n| list: `(message) => JSON string` | `() => array` of `{ name, url }` |\n| save: `({ name, data }) => void` | `(name, data) => void` |\n| clip: `({ name, data, thumbnail }) => void` | `(name, data, thumbnail) => void` |\n\n`data` stays the serialized document JSON string in both. Never call `typograph.setup()` yourself\nnext to the editor: it replaces the editor's document with an empty one. Pass `host` to the\nconstructor instead.\n:::\n\nThe per-panel resolution order and provider details are in\n[Embedding](https://docs.typograph.nl/editor/latest/documents/Embedding.html).","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#manifest--images-fonts-and-swatches","section":"Manifest — images, fonts and swatches","audience":"api","headings":[],"text":"The manifest lists the images, fonts and swatches the editor offers. `host.manifest_data` (see above) takes `{ images, fonts }` and backs the Images tab. `host.manifest`, the pull variant, is a function that returns the same `{ images, fonts }` object, or a promise of one. Both providers pass on only `images` and `fonts`: swatches in either are ignored, and are applied only through `set_manifest_data`. To (re)apply a manifest to the open document at runtime, call `set_manifest_data` on the editor's document. It is **async**, so await it before assuming the assets are in place.\n\n```javascript\ndocument.addEventListener('typograph_editor_ready', async () => {\n  const typograph_document = editor.get_document();\n\n  await typograph_document.set_manifest_data({\n    images: [\n      {\n        id: '019b28fb-a11e-7641-a28f-e978f892ec06',\n        name: 'hero',\n        size: { w: 5213, h: 4160 },\n        original_url:  'https://cdn.example.com/hero/original.jpg',\n        editor_url:    'https://cdn.example.com/hero/editor.webp',\n        print_url:     'https://cdn.example.com/hero/print.jpg',\n        thumbnail_url: 'https://cdn.example.com/hero/thumb.webp',\n      },\n    ],\n    fonts:    [/* { id, name, url } — Open Sans / Source Sans Pro are built in */],\n    swatches: [/* named colours available in the editor */],\n  });\n});\n```\n\nThe document exists once the editor has booted, so the snippets below run inside that same listener.\n\nThe second argument, `preserve_geometry`, says what happens to a placed image whose picture the manifest replaces:\n\n| Value | Image set to Scale to fit or Scale to fill | Image sized by hand |\n| --- | --- | --- |\n| `true` (default) | keeps its fit, recomputed for the new picture | keeps its size: a picture with other proportions is stretched |\n| `false` | reset to Scale to fit, centred | reset to Scale to fit, centred |\n| `'refit'` | keeps its fit, recomputed for the new picture | set to `refit_scale` (Scale to fill by default) |\n\nTo replace pictures, use `'refit'`: the new picture keeps its own proportions and the fit the user chose. A third argument, `refit_scale`, gives images sized by hand Scale to fit instead. It is an argument, not a key in the manifest:\n\n```javascript\nawait typograph_document.set_manifest_data(data, 'refit');\nawait typograph_document.set_manifest_data(data, 'refit', 'Scale_to_Fit');\n```\n\n`'refit'` is available on `latest`. An editor pinned to an older version treats it as `true`, without an error.\n\nTo fetch a manifest from a URL instead, use `set_manifest_url(url, replace = true, preserve_geometry = true, refit_scale)`, which takes the same `preserve_geometry` and `refit_scale` values:\n\n```javascript\nawait typograph_document.set_manifest_url('https://example.com/manifest.json');\n```\n\nPass `replace: false` to merge into the existing manifest rather than clearing it first.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#opening-a-document-programmatically","section":"Opening a Document Programmatically","audience":"api","headings":[],"text":"```javascript\ndocument.addEventListener('typograph_editor_ready', () => {\n  editor.get_document().open_document_url(url);\n});\n```\n\n`window.typograph.get_typograph_document()` returns the same document, for code that has no\n`editor` at hand.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#backend-glue","section":"Backend Glue","audience":"api","headings":[],"text":"The editor expects your backend to:\n\n1. **List user-owned documents / templates / clips**. Your `documents`, `templates` and `clips` providers return `{ name, url }` objects pointing at JSON payloads the editor can fetch.\n2. **Persist save operations**. Your `save_*` providers receive `name` and `data` (the serialized document JSON string) and store them.\n3. **Host images, fonts and swatches** from the manifest at the `*_url` fields you supply.\n\nIf you want those backing URLs to be Typograph-hosted, wire your backend to the Typograph [File Service](https://docs.typograph.nl/guides/api/file-service) and the [presigned-URL upload flow](https://docs.typograph.nl/guides/api/file-service#uploads). Before publishing a document, run it through [`POST /v1/document/validate`](https://docs.typograph.nl/guides/api/document) to catch schema issues early.","agent_tools":[]},{"title":"Editor Integration","url":"https://docs.typograph.nl/guides/integration/editor#related","section":"Related","audience":"api","headings":[],"text":"- [Editor API Reference](https://docs.typograph.nl/editor/latest/modules/editor.html) — generated TypeDoc for the `editor` module\n- [Embedding](https://docs.typograph.nl/editor/latest/documents/Embedding.html) — the authoritative embedding contract for both the editor and the viewer\n- [EditorUI API](https://docs.typograph.nl/editor/latest/documents/API-EditorUI.html) — show, hide and relabel individual chrome controls\n- [Menu API](https://docs.typograph.nl/editor/latest/documents/MenuAPI.html) — adding and removing menu items\n- [Find and Replace](https://docs.typograph.nl/editor/latest/documents/FindReplace.html) — `find()` / `replace_all()`\n- [Page Preview](https://docs.typograph.nl/editor/latest/documents/PagePreview.html) — host-side thumbnails\n- [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer) — the read-only template-filling counterpart, same bundle\n- [Form Integration](https://docs.typograph.nl/guides/integration/form) — letting an end user fill a template in through a form\n- [Templates Integration](https://docs.typograph.nl/guides/integration/templates) — how template URLs are sourced for the `templates` provider\n- [Documents concept](https://docs.typograph.nl/guides/concepts/documents) — the schema the editor reads and writes","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form","section":null,"audience":"api","headings":[],"text":"The Typograph **Form Widget** turns a form you designed in the portal into a fillable form on\nyour own website, with a live preview of the document beside it. Someone fills it in, and you\nreceive the personalised document back, ready to store or turn into a PDF.\n\nIt is one CDN script and one `<div>`. There is no npm package and no backend of ours involved\nat runtime.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#the-one-thing-to-understand-first","section":"The one thing to understand first","audience":"api","headings":[],"text":"**The widget renders what you give it. It fetches nothing from Typograph on its own.**\n\nThree files have to come from your side:\n\n| File | What it is | Where you get it |\n|------|-----------|------------------|\n| Form JSON | The fields, their types, and which template element each one writes to | Download it from the portal: the form's menu, or the Download button in the builder |\n| Template JSON | The document being personalised | The template's presigned URL, fetched server-side |\n| Manifest | The fonts, images and swatches the template depends on | Stored with the template, or assembled by you |\n\nThere is no endpoint that hands out a form to an unauthenticated caller, and no machine\ntoken can read one either. You download the definition from the portal and serve these three\nfrom your own origin, the way you would any other asset. That is a deliberate choice: your\ncustomers' form definitions are not public documents.\n\nThe consequence to plan for: a downloaded definition is a copy. Editing the form in the\nportal does not change what your server sends until you download it again.\n\nThe manifest is the one people forget. A template refers to its fonts and images by id, and\nresolving those ids is the manifest's job. Ship a template without its manifest and the\npreview renders with fallback fonts and missing images — the widget has no way to go looking\nfor them.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#quick-start","section":"Quick start","audience":"api","headings":[],"text":"```html\n<script src=\"https://cdn.typograph.nl/form/latest/form.js\"></script>\n\n<div id=\"typograph-form\"\n     data-typograph-form-url=\"/api/forms/welcome-card.json\"\n     data-typograph-template-url=\"/assets/templates/welcome-card.json\"\n     data-typograph-manifest-url=\"/assets/templates/welcome-card.manifest.json\"\n     data-typograph-locale=\"nl\"></div>\n\n<script>\n  document.addEventListener('typograph_form_ready', () => {\n    const doc = window.typograph.get_typograph_document();\n\n    doc.set_save_template_callback(async ({ name, data, files }) => {\n      // `data` is the template JSON with the user's answers written into it.\n      // `files` are the images they picked, base64, not uploaded anywhere yet.\n      const template = JSON.parse(data);\n\n      for (const file of files) {\n        const blob = await (await fetch(file.data)).blob();\n        const url = await uploadToYourStorage(blob, file.name);\n        // patch `url` into `template` for file.elementId\n      }\n\n      await fetch('/api/personalised', {\n        method: 'POST',\n        body: JSON.stringify(template),\n      });\n    });\n  });\n</script>\n```\n\n:::note\n`doc.set_save_template_callback` on the canvas document still works, but it logs a `[TYP-697]`\ndeprecation warning: the canvas now takes host providers. The handle's\n`widget.set_save_template_callback` (see [JavaScript API](https://docs.typograph.nl/guides/integration/form#javascript-api)) receives the same\n`{ name, data, files }` without the warning. An attribute-only embed like this one has no handle,\nso it keeps `doc.set_save_template_callback` and its warning until the form widget gets a provider\npath.\n:::\n\nThat is the whole integration. Everything below is detail.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#what-happens-when-the-user-submits","section":"What happens when the user submits","audience":"api","headings":[],"text":"Nothing leaves the browser until you send it. While the form is being filled in, images the\nuser picks are previewed client-side as base64 and the template is mutated in memory.\n\nOn submit, your callback fires once per template with three things:\n\n- `name` — the template's name\n- `data` — the mutated template JSON, as a string\n- `files` — `{ elementId, name, data }` for every image the user chose, base64\n\nYou upload the files, patch the resulting URLs into the template, and then store it or\ngenerate a PDF from it. The widget deliberately does not upload anything itself: it does not\nknow your storage, and your customers' uploads should not pass through us.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#what-the-widget-checks-first","section":"What the widget checks first","audience":"api","headings":[],"text":"Your callback does not fire until the answers pass. The form is marked `novalidate` on\npurpose, because the browser's own validation popups cannot be styled and speak the\nbrowser's language rather than the one you configured, so the widget checks the rules\nitself and shows them in its own styles and locale.\n\nIt enforces what the form's author set per field: `required`, `min_length` and\n`max_length` on text, `min`, `max` and `step` on numbers, and `max_file_size` on images.\nAn oversized image is refused as soon as it is chosen, not held back until submit.\n\nIf something fails, the offending fields are marked, the first is focused, and nothing\nis handed over. You do not need to re-check these rules before using the payload.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#when-a-field-does-nothing","section":"When a field does nothing","audience":"api","headings":[],"text":"Every field drives one template element through its `element_uuid`, the element's `uuid`. A field can end up\ndriving nothing at all, and it looks perfectly normal while doing so: it was never\nlinked, it names an element this template does not contain, or it is the wrong sort of\nfield for the element it names — a text field on a rectangle, say.\n\nThe widget reports this to the browser console as soon as the template has loaded,\nnaming each field and what its element actually is. Open the console the first time you\nwire a form up; a form that \"does nothing when I type\" is almost always this.\n\nAdd `data-typograph-debug=\"true\"` (or `debug: true`) to mark those fields in the form as\nwell. Leave it off in production: it explains a problem only the form's author can fix,\nand your visitors cannot act on it.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#configuration","section":"Configuration","audience":"api","headings":["Data attributes","JavaScript API"],"text":"Every option exists as a data attribute and as a JavaScript option. Use attributes for the\ncommon case, the JavaScript API when you need more than one template or want the instance\nhandle.\n\n### Data attributes\n\n| Attribute | Description |\n|-----------|-------------|\n| `data-typograph-form-url` | Form JSON URL (required) |\n| `data-typograph-template-url` | Template JSON to preview and personalise |\n| `data-typograph-manifest-url` | Manifest with fonts, images and swatches |\n| `data-typograph-asset-base-url` | Absolute base for relative asset paths |\n| `data-typograph-images-url` | JSON array of library images, for image fields set to `library` or `both` |\n| `data-typograph-styles-url` | Stylesheet injected after the widget's own |\n| `data-typograph-show-preview` | `\"false\"` hides the canvas preview |\n| `data-typograph-locale` | `en` or `nl` (default `en`) |\n| `data-typograph-canvas-url` | Canvas viewer bundle, for staging against a different build |\n| `data-typograph-debug` | `\"true\"` marks fields that are wired to nothing. Off by default, and meant for while you build |\n| `data-typograph-step-nav` | `\"host\"` stops the widget drawing Back/Next for a form with steps, leaving the page to do it |\n\n### JavaScript API\n\n```js\nconst widget = TypographForm.init({\n  container: '#my-form',\n  formUrl: '/api/forms/welcome-card.json',\n  templateUrls: ['/assets/templates/welcome-card.json'],\n  manifestUrl: '/assets/templates/welcome-card.manifest.json',\n  locale: 'nl',\n  onChange: (values) => console.log(values),\n});\n\nwidget.set_save_template_callback(({ name, data, files }) => { /* … */ });\n```\n\n`init` returns a handle:\n\n```js\nwidget.getValues()            // { uuid: value } as entered right now\nwidget.getMutatedTemplates()  // the template JSON(s) with those values applied\nwidget.setLocale('en')        // switch language, keeping what was typed\nwidget.reset()                // clear the fields, back to the first step\nwidget.destroy()              // remove it from the page\n\nwidget.getStep()              // { index, total, id, name, isFirst, isLast }\nwidget.nextStep()             // check this step and go on; false if it did not move\nwidget.previousStep()         // one back, no checking\n```","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#forms-with-steps","section":"Forms with steps","audience":"api","headings":["When the page owns the button"],"text":"A form built with steps in the portal is walked through one step at a time. The widget\ndraws a step bar, shows one group of fields, and puts the preview on the spread that\nstep is about. Nothing about the embed changes: the steps come out of the form JSON.\n\nGoing on checks the fields of the step you are leaving, unless the step says otherwise.\nSubmitting checks the whole form — a step may be marked as not blocking, so the last\nword is still the submit — and if the first problem sits on an earlier step, the widget\ngoes back to it rather than reporting an error you cannot see.\n\nA form of one step, or a form with no steps at all, behaves exactly as it always has:\nno step bar, no Next, one submit button.\n\nFrom the keyboard, Next is gone on the last step and Back on the first. When the button\nthat had focus goes, focus moves to the name of the step now on screen, and Tab goes on\nfrom there to its first field.\n\n### When the page owns the button\n\nIf your page hides the widget's submit button and puts its own underneath — the usual\narrangement for a webshop — the widget's Next would be a second button doing the same\nthing. Turn its navigation off and drive it yourself:\n\n```js\nconst widget = TypographForm.init({\n  container: '#my-form',\n  formUrl: '/api/forms/welcome-card.json',\n  stepNav: 'host',\n  onStepChange: (step) => {\n    button.textContent = step.isLast ? 'Add to basket' : 'Next';\n    backButton.hidden = step.isFirst;\n  },\n});\n\nbutton.addEventListener('click', () => {\n  if (widget.getStep().isLast) {\n    form.requestSubmit();   // the save callback fires\n  } else {\n    widget.nextStep();\n  }\n});\n```\n\n`onStepChange` also fires once when the form first renders, so the button has the right\nlabel before anything is touched.\n\nA page that just calls `requestSubmit()` and knows nothing about steps keeps working:\non a step that is not the last, submitting means going on. Only the button's label will\nbe wrong, which is what `onStepChange` is for.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#giving-image-fields-a-library-to-pick-from","section":"Giving image fields a library to pick from","audience":"api","headings":[],"text":"An image field can let the visitor upload their own picture, choose from a set you\nsupply, or both. Which of the three is set per field in the form builder; your side of\nit is supplying the set.\n\nHand the widget a list of images and every field set to *library* or *both* gets a\npicker. Without a list those fields quietly fall back to upload, so a missing library\nnever breaks a form.\n\n```js\nTypographForm.init({\n  container: '#typograph-form',\n  formUrl: '/forms/welcome-card.json',\n  templateUrls: ['/templates/welcome-card.json'],\n  images: [\n    { name: 'Mountains', thumbnail_url: '/img/mountains-thumb.jpg', web_url: '/img/mountains.jpg' },\n    { name: 'Coast',     thumbnail_url: '/img/coast-thumb.jpg',     web_url: '/img/coast.jpg' },\n  ],\n});\n```\n\nFor a longer or a changing catalogue, serve the same array as JSON and point at it\ninstead — with `imagesUrl`, or `data-typograph-images-url` on the container:\n\n```json\n[\n  { \"name\": \"Mountains\", \"thumbnail_url\": \"/img/mountains-thumb.jpg\", \"web_url\": \"/img/mountains.jpg\" },\n  { \"name\": \"Coast\", \"thumbnail_url\": \"/img/coast-thumb.jpg\", \"web_url\": \"/img/coast.jpg\" }\n]\n```\n\n| Field | Required | What it is |\n|-------|----------|------------|\n| `name` | Yes | Shown under the thumbnail, and what the visitor picks by |\n| `thumbnail_url` | Yes | The picker's preview. Keep it small; this is the one loaded for every image at once |\n| `web_url` | Recommended | The image placed in the document. Falls back to the thumbnail when absent, which will look soft in print |\n| `print_url` | No | A higher-resolution rendition for the printed result |\n| `editor_url` | No | The rendition the canvas preview loads |\n| `id` | No | Your own reference, passed through untouched |\n\nTwo things worth knowing before you build the list.\n\n**A library pick is not an upload.** The chosen image's URLs are written straight into\nthe document you receive, and it does **not** appear in `payload.files` — there is\nnothing for you to store, because the file was already yours. Uploads still arrive as\nfiles, exactly as before.\n\n**The URLs must resolve from the page the widget runs on.** They are used as given, so\na relative path is resolved against your page, not against ours.\n\nThe picker opens as a dialog that works from the keyboard: focus moves into it, onto the\nimage already chosen for the field or else the first one, Tab stays inside it, and Escape\ncloses it. However it closes, focus goes back to the button that opened it.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#styling","section":"Styling","audience":"api","headings":[],"text":"The form renders inside a Shadow DOM, so its styles never touch your page and your CSS never\ntouches it. Two supported ways in:\n\n**Custom properties**, which inherit through the shadow boundary. Set them on the widget\nelement or any element around it, in its `style` attribute or in your own stylesheet; the\nwidget's own values apply only to a property you did not set:\n\n```html\n<div data-typograph-form-url=\"…\"\n     style=\"--tw-primary:#0aa; --tw-radius:2px; --tw-border:#ddd;\"></div>\n```\n\n```css\n.product-page { --tw-primary: #0aa; }\n```\n\nBecause the widget reads the `--tw-*` tokens from your page, a token your page already\nsets for something else reaches the form too.\n\nThe widget keeps the inline style of the element you embed it in. It sets only the\nfive properties that put the form and the preview side by side (`display`,\n`flex-direction`, `flex-wrap`, `gap`, `align-items`), and `destroy()` gives the element\nits own `style` back.\n\n`--tw-primary`, `--tw-primary-hover`, `--tw-primary-subtle`, `--tw-radius`, `--tw-border`,\n`--tw-bg`, `--tw-label`, `--tw-hint`, `--tw-error`, `--tw-focus` (the keyboard focus ring),\n`--tw-focus-ring` (the halo of a picked library image), `--tw-input-padding-y`,\n`--tw-input-padding-x`.\n\n**The focus ring.** Every control shows a solid ring, 2px wide and 2px clear of it, when it is\nreached with the keyboard, and none when a button is clicked with the mouse. Its colour is\n`--tw-focus`, `#2164ff` by default. Set it when the ring would not stand out against your\nbackground, for example white on a dark `--tw-bg`. Keep it at 3:1 or more against what\nsurrounds the controls.\n\n**A stylesheet**, for structural changes: point `data-typograph-styles-url` at a CSS file and\nit is injected into the shadow root after the base styles, so your rules win. In it, set a\ncustom property under `:host` for the whole form.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#before-you-go-live","section":"Before you go live","audience":"api","headings":[],"text":"- The form is **published**. A draft form is a portal-only object; the widget will not render\n  one.\n- The manifest is served alongside the template, and its font and image URLs resolve from the\n  page the widget runs on.\n- `jsColorEngineWeb.js` sits next to `form.js` if you host the bundle yourself. Serving from\n  our CDN handles this for you.\n- Your save callback is registered before the user can submit — inside the\n  `typograph_form_ready` listener, not after an async fetch that may still be pending.\n- You have tested with an image field. That is the path where the base64 handover, your\n  upload, and the URL patching all have to line up.\n- The browser console is clean when the form loads. A warning there means a field is wired\n  to nothing and will silently do nothing when filled in. The one exception is the `[TYP-697]`\n  deprecation warning from `doc.set_save_template_callback`; see the note under the quick start.\n- If you supply an image library, its thumbnails load from the page the widget runs on. A\n  broken path shows an empty picker rather than an error.","agent_tools":[]},{"title":"Form Integration","url":"https://docs.typograph.nl/guides/integration/form#field-types","section":"Field types","audience":"api","headings":[],"text":"Text, textarea, number, date, colour, dropdown, radio, checkbox, toggle, and image. Each\nfield names the template element it writes to and what it does to it: replace the text, load\nan image, or set a fill colour.\n\nSome template elements cannot be personalised, and the portal refuses to link them: elements\nlocked in the editor, text inside a clip, and text frames that overflow into another frame.\nThose restrictions are enforced when the form is built, so a published form never contains a\nfield that cannot work.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates","section":null,"audience":"api","headings":[],"text":"Templates are the reusable design files in Typograph — created in the Editor, stored in the File Service, rendered by the Publisher. This page covers the API surface around them; see [Concepts → Documents](https://docs.typograph.nl/guides/concepts/documents) for the underlying JSON schema.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#lifecycle","section":"Lifecycle","audience":"api","headings":[],"text":"```\n[Editor creates]\n      ↓\nPOST /v1/file/projects/{projectId}/templates   ← persist (project-scoped)\n      ↓\nPATCH /v1/file/templates/{id}                  ← autosave changes\n      ↓\nPOST /v1/document/validate                     ← pre-flight validation\n      ↓\nPOST /v1/publisher/jobs                        ← render to PDF / image\n      ↓\nGET  /v1/publisher/jobs/{id}/files             ← download output (or wait for webhook)\n```\n\nFile Service endpoints require a **user token** with `file`, `file:read`, `file:write`, or `file:delete` as appropriate, except the reads an integration needs, which also accept a client token: [Token Types](https://docs.typograph.nl/guides/api/token-types#endpoints-accepting-either-token-type-any) lists them.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#template-endpoints","section":"Template Endpoints","audience":"api","headings":[],"text":"The authoritative list is in [File Service → Templates](https://docs.typograph.nl/guides/api/file-service#templates). In short:\n\n| Endpoint | Method | Scopes | Purpose |\n|----------|--------|--------|---------|\n| `/v1/file/projects/{projectId}/templates` | GET | `file:read` | List templates in a project |\n| `/v1/file/projects/{projectId}/templates` | POST | `file:write` | Create a template in a project |\n| `/v1/file/templates/{id}` | GET | `file:read` | Read a template by ID |\n| `/v1/file/templates/{id}` | PATCH | `file:write` | Update |\n| `/v1/file/templates/{id}/touch` | PATCH | `file:write` | Bump modified timestamp (recency) |\n| `/v1/file/templates/{id}/{fileType}/download-url` | GET | `file` / `file:read` | Short-lived link to read the file — open to an app key |\n| `/v1/file/templates/{id}/{fileType}/presigned-url` | POST | `file:write` | Presigned upload URL — `fileType` is `file` or `thumbnail` |\n\nTemplates are created and listed under their owning project, then addressed by their own UUID. Deleting a template happens through the user's trash flow, not a direct `DELETE`. Collaboration (comments, replies, resolve, invitations, roles) lives under `/v1/file/templates/{templateId}/comments/...` and `/v1/file/template/{templateId}/invitations` — see the File Service page.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#wiring-templates-into-the-editor","section":"Wiring Templates Into the Editor","audience":"api","headings":[],"text":"The [Editor](https://docs.typograph.nl/guides/integration/editor) loads templates through the `templates` host provider. The editor asks your page for the available templates; you fetch those from your backend (which in turn talks to the File Service) and return them.\n\n```javascript\nnew Editor({\n  container: '#app',\n  host: {\n    templates: async () => {\n      // Your backend resolves the current user's templates via\n      // GET /v1/file/projects/{projectId}/templates and returns them.\n      const response = await fetch('/api/list-templates');\n      return response.json(); // [{ name, url }, …]\n    },\n    // Plus documents and clips — same pattern.\n  },\n  legacy_php_fallback: false,\n});\n```\n\nSee [Editor → Host Providers](https://docs.typograph.nl/guides/integration/editor#host-providers) for the full provider set.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#template-variables--data-binding","section":"Template Variables / Data Binding","audience":"api","headings":["Server-side merge via CSV"],"text":"A template's data fields are its named text frames and images, and the `{{key}}` placeholders inside its text: the [merge fields](https://docs.typograph.nl/guides/api/file-service#merge-fields) that `GET /v1/file/templates/{id}/fields` lists. The Publisher fills them from a CSV, one output per row.\n\n### Server-side merge via CSV\n\nSubmit the template plus a CSV file whose header names the fields. The Publisher produces one output per CSV row (as child jobs of the parent):\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/publisher/jobs \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"input\": {\n      \"template\": { \"url\": \"https://example.com/template.json\" },\n      \"manifest\": { \"url\": \"https://example.com/manifest.json\" },\n      \"csv\":      { \"url\": \"https://example.com/rows.csv\" }\n    },\n    \"output\": {\n      \"upload\": { \"url\": \"https://example.com/upload-target\" },\n      \"config\": { \"type\": \"pdf\" }\n    }\n  }'\n```\n\nSee [Publisher → What the CSV holds](https://docs.typograph.nl/guides/api/publisher#what-the-csv-holds) for the header and its rules, and [Publisher](https://docs.typograph.nl/guides/api/publisher) for the full job schema including watermark, DPI, page ranges, and crop marks.","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#validating-before-render","section":"Validating Before Render","audience":"api","headings":[],"text":"Always validate before submitting a publisher job — the publisher rejects malformed templates with a `failed` job status (and charges rate-limit either way).\n\n```bash\ncurl -X POST https://api.typograph.nl/v1/document/validate \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"template\": { \"url\": \"https://example.com/template.json\" },\n    \"version\": \"1.0.0\"\n  }'\n```\n\nThe response is `{ \"violations\": [...], \"has_violations\": false }`: `has_violations` is `false` when the template is valid. A template's `download-url` gives the bundle the editor saved, which `/validate` takes as it is. See [Document Service](https://docs.typograph.nl/guides/api/document).","agent_tools":[]},{"title":"Templates","url":"https://docs.typograph.nl/guides/integration/templates#related","section":"Related","audience":"api","headings":[],"text":"- [Editor Integration](https://docs.typograph.nl/guides/integration/editor) — authoring experience\n- [Viewer Integration](https://docs.typograph.nl/guides/integration/viewer) — read-only template filling\n- [File Service → Templates](https://docs.typograph.nl/guides/api/file-service#templates) — full endpoint reference\n- [Publisher](https://docs.typograph.nl/guides/api/publisher) — turn a template into a PDF or image","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer","section":null,"audience":"api","headings":[],"text":"The Typograph **Viewer** fills templates in. It is the read-only counterpart of the [Editor](https://docs.typograph.nl/guides/integration/editor) — same rendering engine, same bundle family, a much smaller interface: no menu bar, no parameter bar, and a floating toolbar that follows whatever is selected.\n\nThe Viewer is not the Editor with things switched off. It is a separate script (`viewer.js`) built for one job: letting an end user complete a template an author already designed.\n\nThere is no npm package — include the script from the CDN.","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#quick-start","section":"Quick Start","audience":"api","headings":[],"text":"```html\n<!DOCTYPE html>\n<html translate=\"no\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Typograph Viewer</title>\n  <script src=\"https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js\"></script>\n</head>\n<body>\n  <!-- The viewer builds itself inside this container. It must have a height. -->\n  <div id=\"app\" style=\"position:absolute; top:0; left:0; right:0; bottom:0;\"></div>\n\n  <script type=\"module\">\n    import { Viewer } from 'https://cdn.typograph.nl/editor/latest/viewer.js';\n    const viewer = new Viewer({ container: '#app' });\n  </script>\n</body>\n</html>\n```\n\nTwo scripts, no markup to author. The colour engine (`jsColorEngineWeb.js`) is a bare-global script and stays your responsibility; everything else — the chrome, the stylesheet, the webfont — the bundle brings itself.\n\n`Viewer` is also exposed as `window.Typograph.Viewer` if you would rather not use a module script.\n\n:::note\n`viewer.js` does nothing until you construct a `Viewer`. This differs from `editor.js`, which mounts itself on `document.body` when nothing constructs it. A page can therefore load the viewer bundle and decide at runtime whether to mount it.\n:::","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#container-requirements","section":"Container Requirements","audience":"api","headings":["container","The container must have a height","Everything stays inside the container","Construct it early"],"text":"### `container`\n\nA CSS selector, a bare element id, or an `HTMLElement` — all three work:\n\n```javascript\nnew Viewer({ container: '#app' });                          // selector\nnew Viewer({ container: 'app' });                           // bare id\nnew Viewer({ container: document.getElementById('app') });  // element\n```\n\nIf nothing matches, a warning is logged and the viewer mounts on `document.body` rather than failing silently. Omit `container` entirely and you get the full-page viewer.\n\n### The container must have a height\n\n**This is the one requirement.** The layout is absolutely positioned and resolves against the container's box, so a container with no height gives you a viewer with no height. A plain `<div id=\"app\"></div>` collapses to zero.\n\n```css\n#app { position: absolute; top: 44px; left: 24px; right: 24px; bottom: 24px; }\n```\n\n```css\n#app { height: 90vh; }\n```\n\n```css\n/* Inside a flex or grid parent that gives it a track. */\n#app { min-height: 0; }\n```\n\nThe viewer handles the rest: an unpositioned container is given `position: relative`, and a container with visible overflow is given `overflow: hidden` so nothing escapes the box you allotted. Both are restored on `destroy()`.\n\n### Everything stays inside the container\n\nThe floating toolbar, its palettes and the swatch picker are all confined to the box you gave the viewer — none of them can be drawn over the rest of your page.\n\n### Construct it early\n\nConstruct the `Viewer` before the window load event. A module script in the page body is early enough.","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#script-url","section":"Script URL","audience":"api","headings":[],"text":"| Channel | URL |\n|---------|-----|\n| Latest stable | `https://cdn.typograph.nl/editor/latest/viewer.js` |\n| Minified | `https://cdn.typograph.nl/editor/latest/viewer.min.js` |\n| Colour engine | `https://cdn.typograph.nl/editor/latest/scripts/jsColorEngineWeb.js` |\n\nThe viewer ships from the **editor** bundle's CDN folder — it is a second entry point of the same build, not a separate product path. Load `viewer.js` as a **module** (`<script type=\"module\" src=\"...\">`).","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#options","section":"Options","audience":"api","headings":[],"text":"```javascript\nnew Viewer({\n    container:   '#app',\n    show_rulers: true,          // default true\n    page_bar: {\n        visible:          true, // the bar itself\n        show_name:        true, // each page's name\n        show_page_number: true, // the number badge\n        show_add_page:    true, // the \"new page\" control\n    },\n});\n```\n\n| Option | Type | Default | What it does |\n|--------|------|---------|--------------|\n| `container` | selector \\| id \\| `HTMLElement` | `document.body` | Where the viewer is built. |\n| `show_rulers` | `boolean` | `true` | Show the rulers. |\n| `page_bar.visible` | `boolean` | canvas default | Show the page bar at all. |\n| `page_bar.show_name` | `boolean` | canvas default | Show each page's name. |\n| `page_bar.show_page_number` | `boolean` | canvas default | Show the page-number badge. |\n| `page_bar.show_add_page` | `boolean` | canvas default | Offer the \"new page\" control. |\n| `host` | `object` | — | Host providers, the same as for the [Editor](https://docs.typograph.nl/guides/integration/editor). Pass them here, not through a `typograph.setup()` call of your own: the viewer calls `setup()` itself. |\n| `legacy_php_fallback` | `boolean` | `true` on `latest` | `false`: no `php/*.php` request; only providers from `host` are used. |\n| `locale` | `'en'` \\| `'nl'` | `data-typograph-locale` on `<html>`, else `'en'` | The language of the viewer's own texts, as for the [Editor](https://docs.typograph.nl/guides/integration/editor#the-language). |\n\nAll options are optional; omit any and the canvas default applies.\n\n`page_bar` matters more than it looks. The page bar is the viewer's only navigation, but a filled-in template is not an authoring surface — the page names it carries are the template author's, not your user's, and there may be nothing to number. Turn off what your product should not show.","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#what-the-user-can-edit","section":"What the User Can Edit","audience":"api","headings":[],"text":"Permissions are answered by the template, not by the viewer. An element with no authored `edit_options` falls back to the viewer default policy:\n\n| Interaction | Default in the viewer |\n|-------------|----------------------|\n| Editing text | Allowed |\n| Moving, scaling, rotating an image **inside** its frame | Allowed |\n| Moving, resizing, rotating the **frame** itself | Denied |\n\nGrant frame edits in a template and the same code allows them, with no change to your host. This is why the viewer is described as a different profile rather than a restricted editor.\n\nThe toolbar floats and follows the selection, showing the controls for whatever is selected and undo/redo when nothing is. Drag it by its handle and it stays where you put it — following the selection is a convenience, your placement is an instruction.","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#what-the-viewer-does-not-have","section":"What the Viewer Does Not Have","audience":"api","headings":[],"text":"Absent by construction — a template cannot turn these on:\n\n- No menu bar, content bar or parameter bar\n- No path tool, marquee, guides or multi-select\n- No numeric position/size entry — that is the parameter bar's job; direct manipulation is the whole interface\n- No image picker. React to the selection event and swap the image through the manifest API — what replaces it belongs to your application, not to Typograph\n- No Find and Replace dialog, because there is no menu bar to carry it. The API behind it is canvas-level and works here unchanged: `window.typograph.find()` and `replace_all()` are available in a viewer embed — see [Find and Replace](https://docs.typograph.nl/editor/latest/documents/FindReplace.html). If the filling in happens server-side before the document reaches the viewer, [TextModel](https://docs.typograph.nl/editor/latest/documents/TextModel.html) is the same job done there","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#lifecycle","section":"Lifecycle","audience":"api","headings":[],"text":"```javascript\nconst viewer = new Viewer({ container: '#app' });\n\nviewer.get_document();   // the canvas document, once booted\nviewer.destroy();        // remove the viewer's DOM and styles\n```\n\nRe-mounting after `destroy()` is not supported — reload the page instead.\n\nThe viewer emits the same canvas lifecycle events as the editor, including `typograph_editor_ready` once it has finished setting up and `typograph_document_opened` when a document loads.","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#authentication","section":"Authentication","audience":"api","headings":[],"text":"The viewer renders a document JSON payload that your page or server already resolved — it does not call Typograph APIs on your behalf. If your template is protected behind the [File Service](https://docs.typograph.nl/guides/api/file-service), fetch the JSON server-side with a user token and hand the data to the viewer.\n\nClient tokens cannot read templates from `/v1/file/*` — see [Token Types](https://docs.typograph.nl/guides/api/token-types).","agent_tools":[]},{"title":"Viewer Integration","url":"https://docs.typograph.nl/guides/integration/viewer#related","section":"Related","audience":"api","headings":[],"text":"- [Viewer API Reference](https://docs.typograph.nl/editor/latest/modules/viewer.html) — generated TypeDoc for the `viewer` module, including the [`Viewer` class](https://docs.typograph.nl/editor/latest/classes/viewer.Viewer.html)\n- [Embedding](https://docs.typograph.nl/editor/latest/documents/Embedding.html) — the authoritative embedding contract for both the editor and the viewer\n- [EditorUI API](https://docs.typograph.nl/editor/latest/documents/API-EditorUI.html) — the `gEditorUI` keys used to show/hide individual toolbar controls\n- [Editor Integration](https://docs.typograph.nl/guides/integration/editor) — the full authoring counterpart, same bundle\n- [Templates Integration](https://docs.typograph.nl/guides/integration/templates) — how template URLs are sourced\n- [Documents concept](https://docs.typograph.nl/guides/concepts/documents) — the schema the viewer reads","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support","section":null,"audience":"api","headings":[],"text":"We're here to help you succeed with Typograph. Choose the support option that works best for you.","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#contact-us","section":"Contact Us","audience":"api","headings":["Email Support","Sales Inquiries"],"text":"### Email Support\n\nFor technical questions, bug reports, or account issues:\n\n**[support@typograph.nl](mailto:support@typograph.nl)**\n\nOur support team typically responds within 24 hours on business days.\n\n### Sales Inquiries\n\nFor pricing, enterprise plans, or partnership opportunities:\n\n**[sales@typograph.nl](mailto:sales@typograph.nl)**","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#resources","section":"Resources","audience":"api","headings":["Documentation","API Reference","AI Integration"],"text":"### Documentation\n\n- **[Getting Started](https://docs.typograph.nl/guides/getting-started/installation)** - Set up Typograph in minutes\n- **[API Overview](https://docs.typograph.nl/guides/api/overview)** - Learn how to use the API\n- **[Portal Guide](https://docs.typograph.nl/portal/)** - Master the Typograph Portal\n\n### API Reference\n\n- **[OpenAPI Reference](https://docs.typograph.nl/api-reference)** - interactive Swagger UI for exploring endpoints\n- **[Authentication Guide](https://docs.typograph.nl/guides/api/authentication)** - OAuth 2.0 setup\n- **[Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting)** - Understand API limits\n\n### AI Integration\n\n- **[AI Agents](https://docs.typograph.nl/guides/api/mcp-server)** - Connect AI agents via MCP","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#status","section":"Status","audience":"api","headings":[],"text":"Check the current status of Typograph services:\n\n**[status.typograph.nl](https://status.typograph.nl)**\n\nSubscribe to status updates to get notified about incidents and maintenance.","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#frequently-asked-questions","section":"Frequently Asked Questions","audience":"api","headings":["Account & Billing","API & Integration","Document Generation"],"text":"### Account & Billing\n\n<details>\n<summary>How do I sign in to the Portal?</summary>\n\nGo to [portal.typograph.nl](https://portal.typograph.nl). Sign in with your email and verification code, or with Google or Microsoft SSO — the Typograph Portal does not use passwords.\n\n</details>\n\n<details>\n<summary>The sign-in page says \"Verify you are human\", or that it couldn't confirm I'm a person</summary>\n\nSigning up and asking for a sign-in code are protected against bots by a security check from Cloudflare. Almost everyone passes it without seeing anything. Now and then it asks you to tick \"Verify you are human\" before the form is sent: tick it, and the form goes on by itself.\n\nIf the page says it couldn't confirm you're a person, or that the security check could not load:\n\n- Try again, or reload the page.\n- Turn off an extension that blocks content (an ad or script blocker) for `id.typograph.nl`, or try another browser.\n- Check that your network does not block `challenges.cloudflare.com`.\n\nSigning in with Google or Microsoft does not use this check. Still stuck? Email [support@typograph.nl](mailto:support@typograph.nl).\n\n</details>\n\n<details>\n<summary>I was signing in to another app and landed on a Typograph error page</summary>\n\nMany apps let you sign in through Typograph, so you can meet a Typograph page without having heard of us. When that sign-in fails, the page names the app and how to reach it, by email or on its website: start there, as the app knows your account with it. Mention the error reference the page shows. If the app cannot help, email [support@typograph.nl](mailto:support@typograph.nl) with that reference.\n\n</details>\n\n<details>\n<summary>The sign-up page says too many accounts were created from my network</summary>\n\nTo stop accounts being created in bulk, one network can create a limited number of accounts an hour, and ask for a limited number of sign-in codes. The page says when you can try again. Colleagues signing up together behind one office connection can hit this: wait until the time shown, or sign up with Google or Microsoft, which does not count toward it.\n\n</details>\n\n<details>\n<summary>I signed up but never signed in. What happens to my account?</summary>\n\nYour account is confirmed the first time you sign in with the code we email you, or with Google or Microsoft. If nobody signs in within a week of signing up, we email the address once to ask you to confirm. If there is still no sign-in seven days after that email, we delete the account and the organization it came with, and you can sign up again with the same address. The email gives the exact date.\n\nTo keep the account, sign in at [portal.typograph.nl](https://portal.typograph.nl) before that date: we email you a new code. If you did not create the account, you do not have to do anything.\n\n</details>\n\n<details>\n<summary>How do I upgrade my plan?</summary>\n\nPro and Enterprise are on request. In the Portal, open your organization's subscription and choose **Change plan**, then **Ask for Pro** or, for Enterprise, **Talk to an expert**, or mail [sales@typograph.nl](mailto:sales@typograph.nl). Sales agrees the price with you and then moves your organization to the new plan.\n\n</details>\n\n<details>\n<summary>How do I cancel my subscription?</summary>\n\nEmail [support@typograph.nl](mailto:support@typograph.nl) with your account details and cancellation request.\n\n</details>\n\n### API & Integration\n\n<details>\n<summary>How do I get API credentials?</summary>\n\n1. Log in to the [Typograph Portal](https://portal.typograph.nl).\n2. Open **Settings → Organizations → _your organization_ → Apps**.\n3. Click **+ Create App**, fill in a name, optional redirect URIs, and the scopes you need.\n4. Copy the **Client ID** and **Client Secret** shown after saving (the secret is only displayed once).\n\nSee the [Authentication Guide](https://docs.typograph.nl/guides/api/authentication) for details.\n\n</details>\n\n<details>\n<summary>What are the API rate limits?</summary>\n\nRate limits are **tiered by your organization's subscription plan** and split across four layers (burst, hourly, daily, monthly) and five endpoint categories (general, file, converter, publisher, webhook), plus `ai` for the portal's AI assistant. Anonymous (unauthenticated) traffic is limited to the `general` category at 100/hour, 1,000/day.\n\nSee [Rate Limiting](https://docs.typograph.nl/guides/api/rate-limiting) for the full matrix and header format.\n\n</details>\n\n<details>\n<summary>Why am I getting a 401 Unauthorized error?</summary>\n\nCommon causes:\n- Access token has expired (tokens last 2 days)\n- Invalid or missing `Authorization` header\n- Token was revoked\n\nSolution: Request a new access token using your credentials or refresh token.\n\n</details>\n\n<details>\n<summary>Why am I getting a 403 Forbidden error?</summary>\n\nYour token doesn't have the required scope. Check which scopes your application needs and request them when obtaining a token.\n\nSee [Scopes](https://docs.typograph.nl/guides/api/scopes) for the full list.\n\n</details>\n\n### Document Generation\n\n<details>\n<summary>How long are generated files available?</summary>\n\nGenerated files are available for **4 days** after job completion. After this period, they expire and return a `410 Gone` status.\n\n</details>\n\n<details>\n<summary>What output formats are supported?</summary>\n\nThe Publisher service supports:\n- **PDF** — print-ready documents\n- **PNG**, **JPG/JPEG**, **WebP** — image exports (one file per page)\n\nSee the [Publisher reference](https://docs.typograph.nl/guides/api/publisher#output-formats) for details.\n\n</details>\n\n<details>\n<summary>How do I track job progress?</summary>\n\nOption 1: Poll the job status endpoint until `status` is `completed` or `failed`.\n\nOption 2: Set up [webhooks](https://docs.typograph.nl/guides/api/webhooks) to receive notifications when jobs complete.\n\n</details>","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#report-an-issue","section":"Report an Issue","audience":"api","headings":["Bug Reports","Feature Requests"],"text":"### Bug Reports\n\nWhen reporting a bug, please include:\n\n1. **Error reference or request ID** - An error page in the Portal or at sign-in shows an **Error reference**: copy it, or choose **Email this reference to support**, which writes a mail with the reference and the page already filled in. For an API call, the request ID is in the `X-Typograph-Request-Id` response header\n2. **Steps to reproduce** - What actions led to the issue\n3. **Expected behavior** - What should have happened\n4. **Actual behavior** - What actually happened\n5. **Screenshots** - If applicable\n\nSend bug reports to [support@typograph.nl](mailto:support@typograph.nl).\n\n### Feature Requests\n\nHave an idea to improve Typograph? We'd love to hear it!\n\nEmail [support@typograph.nl](mailto:support@typograph.nl) with:\n- Description of the feature\n- Use case / problem it solves\n- Any examples or mockups","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#enterprise-support","section":"Enterprise Support","audience":"api","headings":[],"text":"Enterprise customers receive:\n\n- **Priority support** - Faster response times\n- **Dedicated account manager** - Direct line to your success team\n- **Custom integrations** - Help building your integration\n- **SLA guarantees** - Uptime and response time commitments\n- **Training sessions** - Onboarding for your team\n\nContact [sales@typograph.nl](mailto:sales@typograph.nl) to learn more about enterprise plans.","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#office","section":"Office","audience":"api","headings":[],"text":"**Typograph B.V.**\n\nVisiting address and correspondence:\n\nDorpsstraat 50\n3433 CM Nieuwegein\nThe Netherlands\n\n**Chamber of Commerce:** _TBD_","agent_tools":[]},{"title":"Support","url":"https://docs.typograph.nl/guides/support#useful-links","section":"Useful Links","audience":"api","headings":[],"text":"| Resource | URL |\n|----------|-----|\n| Portal | [portal.typograph.nl](https://portal.typograph.nl) |\n| API | [api.typograph.nl](https://api.typograph.nl) |\n| Documentation | [docs.typograph.nl](https://docs.typograph.nl) |\n| Status | [status.typograph.nl](https://status.typograph.nl) |\n| Website | [typograph.nl](https://www.typograph.nl) |","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue","section":null,"audience":"api","headings":[],"text":"What the shop sells, and everything a storefront needs to show it. Every call takes the\n`X-Shop-Key` header (see [Authentication](https://docs.typograph.nl/guides/webshop/overview#authentication)) and answers only with\nwhat the shop has on sale: a product that is hidden, in the trash, or cannot be made right now\nis not in any list and answers `404` on its own.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#endpoints","section":"Endpoints","audience":"api","headings":[],"text":"| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/v1/shop/products` | A page of products, with the filters and sort orders the listing offers |\n| `GET` | `/v1/shop/products/{slug}` | One product, with its variants and the documents the form widget needs |\n| `GET` | `/v1/shop/categories` | Every category, in the shop's order |\n| `GET` | `/v1/shop/languages` | The languages the shop shows, its main language first |\n| `GET` | `/v1/shop/showcase` | The designs shown in each place of the storefront |\n| `GET` | `/v1/shop/search` | Products matching a search |\n| `GET` | `/v1/shop/shipping-methods` | The delivery options, their prices and the VAT rate |","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#list-products","section":"List products","audience":"api","headings":["Filters"],"text":"```bash\ncurl \"https://shop-api.typograph.nl/v1/shop/products?category=birth&theme[]=Birth&sort=price_asc\" \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Accept-Language: en\"\n```\n\n| Parameter | Description |\n|-----------|-------------|\n| `limit`, `offset` | The window, see [Response shapes](https://docs.typograph.nl/guides/webshop/overview#response-shapes) |\n| `category` | A category slug. An unknown one is a `404`, not an empty list |\n| `sort` | `recommended` (the default: the order the shop set in its back office), `popular`, `price_asc` or `price_desc`. Anything else is read as `recommended` |\n| one per filter | The values to filter on, named after the filter: `theme[]=Birth&theme[]=Christmas`. Values of one filter are OR'ed, different filters are AND'ed. A filter that does not exist is ignored |\n\n```json\n{\n  \"data\": [\n    {\n      \"slug\": \"welcome\",\n      \"name\": \"Welcome\",\n      \"badge\": \"New\",\n      \"tags\": { \"theme\": [\"Birth\"], \"colour\": [\"Blue\"] },\n      \"from_price_cents\": 2450,\n      \"preview_image_url\": \"https://shop-api.typograph.nl/designs/…/thumbnail\",\n      \"is_personalizable\": true,\n      \"categories\": [\"birth\"]\n    }\n  ],\n  \"meta\": {\n    \"total\": 1,\n    \"limit\": 25,\n    \"offset\": 0,\n    \"has_more\": false,\n    \"sort\": \"price_asc\",\n    \"sorts\": [\n      { \"value\": \"recommended\", \"label\": \"Recommended\" },\n      { \"value\": \"popular\", \"label\": \"Popular\" },\n      { \"value\": \"price_asc\", \"label\": \"Price, low to high\" },\n      { \"value\": \"price_desc\", \"label\": \"Price, high to low\" }\n    ],\n    \"filters\": { \"theme\": [\"Birth\"], \"colour\": [], \"prijs\": [] },\n    \"facets\": [\n      {\n        \"axis\": \"theme\",\n        \"label\": \"Theme\",\n        \"values\": [\n          { \"value\": \"Birth\", \"name\": \"Birth\", \"count\": 1, \"is_selected\": true },\n          { \"value\": \"Christmas\", \"name\": \"Christmas\", \"count\": 0, \"is_selected\": false }\n        ]\n      }\n    ]\n  }\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `badge` | A short label the shop puts on the product's card, or `null` |\n| `tags` | The filter values this product carries, per filter |\n| `from_price_cents` | The price of the cheapest variant, excluding VAT |\n| `preview_image_url` | The shop's photograph of the product, or else the thumbnail of its design; `null` when there is neither |\n| `is_personalizable` | Whether the product is filled in with the form widget |\n| `categories` | Category slugs |\n\n### Filters\n\nThe filters are the shop's own: the shopkeeper makes them under **Categories and filters**.\nSo do not hard-code them. `meta.facets` lists every filter with every value, in the order the\nshop set, and the same filters come back on every listing, also when a value matches nothing.\nThat keeps a filter sidebar from rearranging itself between pages.\n\n- `axis` is what goes in the query string; `label` is the filter's name to show.\n- `value` is what goes in the query string; `name` is how it reads.\n- `count` is how many products ticking this value would leave, given the other filters already\n  chosen. It says what happens next, not what the category holds.\n- `is_selected` says whether the request asked for it. `meta.filters` repeats the choice per filter.\n\nThe last filter is always `prijs` (it keeps its Dutch name), worked out from the product's price rather than set by hand,\nso it always agrees with what the product costs.\n\n`meta.sorts` is the list of sort orders, with labels in the request's language. Show this list\nrather than a copy of your own.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#get-one-product","section":"Get one product","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/products/welcome \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\nEverything in the list entry, plus:\n\n```json\n{\n  \"data\": {\n    \"slug\": \"welcome\",\n    \"name\": \"Welcome\",\n    \"description\": \"Birth announcement with room for a photo.\",\n    \"print_specification\": { \"paper\": \"320 g handmade board\", \"format\": \"105 x 148 mm\" },\n    \"variants\": [\n      { \"id\": \"0198a3c4-5e6f-7a8b-9c0d-1e2f3a4b5c6d\", \"name\": \"25 cards\", \"sku\": \"WEL-25\", \"price_cents\": 2450 }\n    ],\n    \"form_url\": \"https://shop-api.typograph.nl/products/welcome/form.json\",\n    \"template_url\": \"https://shop-api.typograph.nl/products/welcome/template.json\",\n    \"manifest_url\": \"https://shop-api.typograph.nl/products/welcome/manifest.json\",\n    \"asset_base_url\": \"https://shop-api.typograph.nl/products/welcome/\"\n  }\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `print_specification` | What it is printed on, as label and value, or `null` |\n| `variants` | What can be ordered. Send a variant's `id` with an order line |\n| `form_url`, `template_url`, `manifest_url` | The form, the design and its fonts and images, for the [form widget](https://docs.typograph.nl/guides/webshop/orders#1-show-the-form) |\n| `asset_base_url` | What the design's relative paths resolve against |\n\nThe four addresses are on the shop itself, so the widget reads them without a Typograph\naccount. They answer only the websites on the shop's allowed list (see\n[The product documents](https://docs.typograph.nl/guides/webshop/overview#the-product-documents)).","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#list-categories","section":"List categories","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/categories \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\n```json\n{\n  \"data\": [\n    { \"slug\": \"birth\", \"name\": \"Birth\" },\n    { \"slug\": \"christmas\", \"name\": \"Christmas\" }\n  ],\n  \"meta\": { \"total\": 2, \"limit\": 100, \"offset\": 0, \"has_more\": false }\n}\n```\n\nA category's slug stays the same when the shopkeeper renames it, so it is safe to put in your\nURLs.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#list-languages","section":"List languages","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/languages \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\n```json\n{\n  \"data\": [\n    { \"code\": \"en\", \"name\": \"English\", \"is_default\": true },\n    { \"code\": \"nl\", \"name\": \"Nederlands\", \"is_default\": false }\n  ],\n  \"meta\": { \"total\": 2, \"limit\": 100, \"offset\": 0, \"has_more\": false }\n}\n```\n\nOnly the languages the shopkeeper has made visible, the main one (`is_default`) first. Offer\nthese in your language menu and send the chosen `code` as `Accept-Language`. `name` is the\nlanguage's own name, as the shopkeeper wrote it.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#showcase","section":"Showcase","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/showcase \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\nThe designs a storefront shows in its fixed places, already topped up: for each place, the\ndesigns the shopkeeper chose in the back office that are on sale, in the chosen order, then the\nfirst designs of the catalogue order until the place is full. With nothing chosen, a place is\nsimply the catalogue order. Each design has the same shape as a row of the product list.\n\n```json\n{\n  \"data\": {\n    \"home_banner\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"home_carousel\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"cart_suggestions\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"faq\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"sign_in\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"register\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }],\n    \"contact\": [{ \"slug\": \"welcome\", \"name\": \"Welcome\", \"from_price_cents\": 2450, \"…\": \"…\" }]\n  }\n}\n```\n\nThe places and the most designs each shows: `home_banner` 3, `home_carousel` 12,\n`cart_suggestions` 4, and `faq`, `sign_in`, `register` and `contact` 1. They are fixed in the\nservice: a new place needs a storefront that renders it.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#search","section":"Search","audience":"api","headings":[],"text":"```bash\ncurl \"https://shop-api.typograph.nl/v1/shop/search?query=blue+birth\" \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\nEvery word has to match, so typing more narrows the result. A word matches a product's name,\nits description or the name of one of its categories. Only the first six words count, an empty\n`query` finds nothing, and a search finds at most 60 products.\n\nThe answer is a page of products like `/v1/shop/products`, with `limit` and `offset`, but\nwithout the filters and sort orders in `meta`.","agent_tools":[]},{"title":"Catalogue","url":"https://docs.typograph.nl/guides/webshop/catalogue#delivery-options","section":"Delivery options","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/shipping-methods \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Accept-Language: en\"\n```\n\n```json\n{\n  \"data\": [\n    { \"value\": \"next_day\", \"label\": \"Delivered tomorrow before 12:00\", \"price_cents\": 895, \"vat_rate\": 21 },\n    { \"value\": \"standard\", \"label\": \"Standard delivery, Thursday\", \"price_cents\": 0, \"vat_rate\": 21 },\n    { \"value\": \"pickup\", \"label\": \"Collect from the warehouse\", \"price_cents\": 0, \"vat_rate\": 21 }\n  ],\n  \"meta\": { \"total\": 3, \"limit\": 100, \"offset\": 0, \"has_more\": false }\n}\n```\n\nShow these, and send the chosen `value` as `shipping_method` with the order. `price_cents`\nexcludes VAT; `vat_rate` is the percentage charged over the goods and the delivery together.\nRead them here rather than copying them into your storefront: the order is charged at the\nshop's prices, whatever your page said.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers","section":null,"audience":"api","headings":[],"text":"An order needs no account: a visitor can check out as a guest. An account is for everything\nelse — a shortlist that follows them, and their orders in one place.\n\nThese are the shop's own customers, not Typograph accounts. Someone buying a birth\nannouncement never signs up for a design platform.\n\n**Make these calls from the visitor's browser or from your own server.** Every call after\nsign-in carries the customer's token in `X-Customer-Token`, with the shop key as on every\ncall. A page on a site on the shop's **Allowed websites** list may send that header and the\n`PUT` and `DELETE` of the shortlist (see\n[What a browser may send](https://docs.typograph.nl/guides/webshop/overview#what-a-browser-may-send)). Keep the token for as long\nas the customer stays signed in: in the page, or in your storefront's session when your\nserver makes the calls.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#endpoints","section":"Endpoints","audience":"api","headings":[],"text":"| Method | Path | Description |\n|--------|------|-------------|\n| `POST` | `/v1/shop/customers` | Create an account |\n| `POST` | `/v1/shop/sessions` | Sign in |\n| `DELETE` | `/v1/shop/sessions` | Sign out, on every device |\n| `GET` | `/v1/shop/users/me` | The signed-in customer |\n| `POST` | `/v1/shop/users/me/back-office-link` | A link into the back office, for staff |\n| `PUT` | `/v1/shop/users/me/favorites/{slug}` | Put a product on the shortlist |\n| `DELETE` | `/v1/shop/users/me/favorites/{slug}` | Take it off again |\n| `GET` | `/v1/shop/users/me/orders` | What this customer ordered |","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#create-an-account","section":"Create an account","audience":"api","headings":[],"text":"```bash\ncurl -X POST https://shop-api.typograph.nl/v1/shop/customers \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"emma@example.com\", \"password\": \"at least 8 characters\", \"name\": \"Emma Clarke\"}'\n```\n\n`name` is optional; the password needs at least 8 characters. `201`:\n\n```json\n{\n  \"data\": {\n    \"id\": \"019f7fa1-0c2d-7e3f-8a4b-5c6d7e8f9a01\",\n    \"email\": \"emma@example.com\",\n    \"name\": \"Emma Clarke\",\n    \"favorites\": [],\n    \"is_staff\": false,\n    \"token\": \"…\"\n  }\n}\n```","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#sign-in","section":"Sign in","audience":"api","headings":[],"text":"```bash\ncurl -X POST https://shop-api.typograph.nl/v1/shop/sessions \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"emma@example.com\", \"password\": \"…\"}'\n```\n\n`200`, with the same body as above. A wrong password and an unknown address get the same\nanswer, so sign-in cannot be used to find out who shops here.\n\nSign-in is limited to **five attempts per e-mail address in fifteen minutes**, counted for known\nand unknown addresses alike. Over that it answers `429` `too_many_requests` with a `Retry-After`\nheader in seconds. A successful sign-in clears the count.\n\n**The token** is handed out only here and at sign-up, never on a later read. It works for 14\ndays; after that the calls answer `401` `not_signed_in`, and the customer signs in again.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#sign-out","section":"Sign out","audience":"api","headings":[],"text":"```bash\ncurl -X DELETE https://shop-api.typograph.nl/v1/shop/sessions \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Customer-Token: …\"\n```\n\n`204`, without a body. This ends **every** token of the account, not only the one you sent: a\ntoken is signed rather than stored, so merely forgetting it in your storefront would leave a\ncopy working until it expires. A call with no token, or one that no longer works, answers `401`\n`not_signed_in`.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#the-signed-in-customer","section":"The signed-in customer","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/users/me \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Customer-Token: …\"\n```\n\nThe account as above, without `token`.\n\n- `favorites` are product slugs, in the order they were added.\n- `is_staff` says whether this person has a role in the shop's back office.\n  Use it to show them the way in, with the link below.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#into-the-back-office","section":"Into the back office","audience":"api","headings":[],"text":"The storefront and the back office are two hosts with two sessions. To take somebody who works\nin the back office there without asking for the password again, ask for a link right after\nthey sign in and send the browser to it:\n\n```bash\ncurl -X POST https://shop-api.typograph.nl/v1/shop/users/me/back-office-link \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Customer-Token: …\"\n```\n\n```json\n{\n  \"data\": {\n    \"url\": \"https://shop-api.typograph.nl/login/link?…\",\n    \"expires_at\": \"YYYY-MM-DDT14:06:00+02:00\"\n  }\n}\n```\n\nThe link signs in once and expires after a minute; a new password voids it. Only a token\nhanded out in the last five minutes buys one, so a token that was copied or left open cannot be\nturned into a back-office session later. Without a link, send them to the back office anyway:\nit asks for the password.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#the-shortlist","section":"The shortlist","audience":"api","headings":[],"text":"```bash\ncurl -X PUT https://shop-api.typograph.nl/v1/shop/users/me/favorites/welcome \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Customer-Token: …\"\n```\n\n`PUT` adds a slug and `DELETE` removes it. A slug the shop does not sell answers `404`\n`unknown_product`; a shortlist already\nholding 200 designs answers `422` `too_many_favorites`.\nBoth answer with the shortlist as it now stands:\n\n```json\n{\n  \"data\": { \"favorites\": [\"welcome\", \"confetti\"] }\n}\n```\n\nRead each product with [`GET /v1/shop/products/{slug}`](https://docs.typograph.nl/guides/webshop/catalogue#get-one-product). A\nproduct that is no longer on sale answers `404`; leave it out.","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#order-history","section":"Order history","audience":"api","headings":[],"text":"```bash\ncurl https://shop-api.typograph.nl/v1/shop/users/me/orders \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Customer-Token: …\"\n```\n\n```json\n{\n  \"data\": [\n    {\n      \"number\": \"ST-YYYY-0042\",\n      \"access_token\": \"…\",\n      \"status\": \"new\",\n      \"total_cents\": 2965,\n      \"placed_at\": \"YYYY-MM-DDT14:05:00+02:00\"\n    }\n  ],\n  \"meta\": { \"total\": 1, \"limit\": 100, \"offset\": 0, \"has_more\": false }\n}\n```\n\nThe orders placed with this customer's token (see `X-Customer-Token` on\n[placing an order](https://docs.typograph.nl/guides/webshop/orders#place-the-order)), newest first, at most 100. `meta.total` counts every order of the account, so `meta.has_more` tells you some are older than the 100 in `data`. This list cannot be paged: open an older order with its own token, from its confirmation. They are not\nmatched on the e-mail address, because nothing proves an address belongs to whoever registered\nit: an order placed as a guest never appears here, also not after the customer makes an\naccount. Keep the order's own token to read it back. Each row carries that token, to open the\norder with [`GET /v1/shop/orders/{number}`](https://docs.typograph.nl/guides/webshop/orders#read-an-order-back). Its lines say how the\nprint-ready PDF of each design is doing, and hand out its download once it is ready: see\n[The print file of a line](https://docs.typograph.nl/guides/webshop/orders#the-print-file-of-a-line).","agent_tools":[]},{"title":"Customers","url":"https://docs.typograph.nl/guides/webshop/customers#errors","section":"Errors","audience":"api","headings":[],"text":"| Status | `error` | Meaning |\n|--------|---------|---------|\n| `422` | `invalid_email` | Not a valid e-mail address |\n| `422` | `weak_password` | A password shorter than 8 characters |\n| `422` | `email_taken` | An account with this address already exists |\n| `422` | `invalid_credentials` | The address and the password do not belong together |\n| `401` | `not_signed_in` | No token, or one the shop no longer accepts |\n| `403` | `not_staff` | A back-office link for an account with no role in the back office |\n| `403` | `sign_in_again` | A back-office link for a token older than five minutes |\n| `404` | `unknown_product` | `PUT` of a slug the shop does not sell |\n| `422` | `too_many_favorites` | The shortlist already holds 200 designs |\n| `429` | `too_many_requests` | Five sign-in attempts for this address in fifteen minutes; wait `Retry-After` seconds |","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders","section":null,"audience":"api","headings":[],"text":"A visitor fills in a product with the [Form Widget](https://docs.typograph.nl/guides/integration/form), puts it in a\ncart, and orders it. **The shop has no cart.** The cart lives in the visitor's browser until\ncheckout, and nothing is sent to the shop before the order is placed: a personalisation is\nonly worth storing once someone buys it.\n\n```\n product page          cart (browser)          checkout\n┌──────────────┐     ┌────────────────┐     ┌──────────────────────────────────────┐\n│ form widget  │ ──▶ │ lines + files  │ ──▶ │ 1. POST /v1/shop/uploads             │\n│ fills in the │     │ in IndexedDB   │     │ 2. PUT every picture to its address  │\n│ design       │     │                │     │ 3. POST /v1/shop/orders, with keys   │\n└──────────────┘     └────────────────┘     └──────────────────────────────────────┘\n```\n\nThe order is the moment the shop writes the personalised design down. From then on it is\nfixed: a later change to the design in Typograph does not touch an order already placed.\n\nThere are two ways to build this. The shop serves two JavaScript modules that do all of it,\nand our own storefront uses them. Or you use the form widget yourself and send the same calls.","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#with-the-shops-modules","section":"With the shop's modules","audience":"api","headings":["1. Show the form","2. Place the order"],"text":"The shop serves two ES modules from its own address, which any site may load:\n\n| Module | What it does |\n|--------|-------------|\n| `/js/personalize.js` | `mount()` puts the form widget on a product and saves the result to the cart |\n| `/js/cart-store.js` | The cart in the browser: `listLines()`, `setQuantity()`, `removeLine()`, `countItems()`, `clear()`, and `orderLines()` for checkout |\n\n### 1. Show the form\n\nOn the product page, read the product with\n[`GET /v1/shop/products/{slug}`](https://docs.typograph.nl/guides/webshop/catalogue#get-one-product) and mount the form:\n\n```html\n<script src=\"https://cdn.typograph.nl/form/latest/form.js\"></script>\n\n<div id=\"personalize\"></div>\n\n<script type=\"module\">\n  import { mount } from 'https://shop-api.typograph.nl/js/personalize.js';\n\n  // product = (await GET /v1/shop/products/{slug}).data\n  await mount({\n    host: document.getElementById('personalize'),\n    product: {\n      slug: product.slug,\n      name: product.name,\n      formUrl: product.form_url,\n      templateUrl: product.template_url,\n      manifestUrl: product.manifest_url,\n      assetBaseUrl: product.asset_base_url,\n    },\n    variantId: product.variants[0].id,\n    canvasUrl: 'https://cdn.typograph.nl/editor/latest/viewer.js',\n    locale: 'en',\n    translationsUrl: '',\n    onSaved: () => { location.href = '/cart'; },\n  });\n</script>\n```\n\nWhen the visitor submits the form, `mount()` renders a small preview of the result in the\nbrowser and stores the line, its pictures and the preview in the cart. Without\n`translationsUrl` the widget's texts are the shop's Dutch ones; `''` keeps the widget's own\nEnglish.\n\n### 2. Place the order\n\nAt checkout, `orderLines()` uploads the pictures and previews of every line and answers with\nthe lines in the shape the order wants:\n\n```js\nimport { orderLines, clear } from 'https://shop-api.typograph.nl/js/cart-store.js';\n\nconst SHOP = 'https://shop-api.typograph.nl';\nconst SHOP_KEY = 'shop_pk_…';\n\nconst response = await fetch(`${SHOP}/v1/shop/orders`, {\n  method: 'POST',\n  headers: { 'X-Shop-Key': SHOP_KEY, 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    email,\n    shipping_method: 'standard',\n    shipping_address,\n    lines: await orderLines({ shopUrl: SHOP, shopKey: SHOP_KEY }),\n  }),\n});\n\nconst { number, access_token } = (await response.json()).data;\nawait clear();\n```\n\nKeep `number` and `access_token`: they are how the visitor reads the order back (see\n[Read an order back](https://docs.typograph.nl/guides/webshop/orders#read-an-order-back)).\n\n`orderLines()` throws before it uploads anything when the cart holds more than the shop takes\nin one order (see [the limits](https://docs.typograph.nl/guides/webshop/orders#order-errors)).","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#with-your-own-cart","section":"With your own cart","audience":"api","headings":[],"text":"Use the form widget as described in [Form Integration](https://docs.typograph.nl/guides/integration/form), with the four\naddresses of the product: `form_url` as the form, `template_url` as the template,\n`manifest_url` as the manifest and `asset_base_url` as the asset base.\n\nOn submit, the widget hands you `{ name, data, files }`: the filled-in design as a JSON\nstring, and every picture the visitor chose as `{ elementId, name, data }` with `data` a\nbase64 data URL. Keep them until checkout, then:\n\n1. Ask for an upload address per picture: [`POST /v1/shop/uploads`](https://docs.typograph.nl/guides/webshop/orders#upload-the-pictures).\n2. `PUT` every picture to its address.\n3. Place the order with the design as `template_data` and the keys of the pictures:\n   [`POST /v1/shop/orders`](https://docs.typograph.nl/guides/webshop/orders#place-the-order).\n\nLeave the data URLs in `template_data` as the widget wrote them. The shop puts the address of\nthe stored picture in their place, matched on the picture's `element_uuid` in the order (the\nwidget's `elementId`).","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#upload-the-pictures","section":"Upload the pictures","audience":"api","headings":["Upload errors"],"text":"```bash\ncurl -X POST https://shop-api.typograph.nl/v1/shop/uploads \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"files\": [\n      { \"content_type\": \"image/jpeg\", \"size\": 482113 },\n      { \"content_type\": \"image/webp\", \"size\": 35120 }\n    ]\n  }'\n```\n\n`201`, with one upload address per file, in the order asked:\n\n```json\n{\n  \"data\": {\n    \"uploads\": [\n      {\n        \"key\": \"incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb24\",\n        \"upload_url\": \"https://…\",\n        \"content_type\": \"image/jpeg\",\n        \"size\": 482113\n      },\n      {\n        \"key\": \"incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb25\",\n        \"upload_url\": \"https://…\",\n        \"content_type\": \"image/webp\",\n        \"size\": 35120\n      }\n    ],\n    \"expires_in\": 900,\n    \"max_bytes\": 20971520\n  }\n}\n```\n\nThen `PUT` each file to its `upload_url`, with exactly that `Content-Type`:\n\n```js\nawait fetch(upload.upload_url, {\n  method: 'PUT',\n  headers: { 'Content-Type': upload.content_type },\n  body: blob,\n});\n```\n\n- The type and the size are part of the address's signature. The storage refuses a file of\n  another type or another size than the one you asked for.\n- An address works for `expires_in` seconds: 15 minutes.\n- Pictures are JPEG, PNG, WebP or GIF, at most 20 MB each, and at most 40 per request. A\n  cart with more asks more than once.\n- A picture nobody orders is removed after a day.\n\nYour storefront's website has to be allowed to upload; see\n[Picture uploads](https://docs.typograph.nl/guides/webshop/overview#picture-uploads).\n\n### Upload errors\n\n| Status | `error` | Meaning |\n|--------|---------|---------|\n| `422` | `no_files` | `files` is missing or empty |\n| `422` | `too_many_files` | More than 40 files in one request |\n| `422` | `unsupported_type` | A type other than JPEG, PNG, WebP or GIF |\n| `422` | `upload_too_large` | A file without a size, or over 20 MB |\n| `429` | `too_many_requests` | Over 30 requests in an hour from this visitor; `Retry-After` says when to try again |","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#place-the-order","section":"Place the order","audience":"api","headings":["Order errors"],"text":"```bash\ncurl -X POST https://shop-api.typograph.nl/v1/shop/orders \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @order.json\n```\n\n```json\n{\n  \"email\": \"emma@example.com\",\n  \"note\": \"Please deliver before Friday\",\n  \"shipping_method\": \"standard\",\n  \"payment_method\": \"invoice\",\n  \"payment_reference\": \"PO-1234\",\n  \"shipping_address\": {\n    \"name\": \"Emma Clarke\",\n    \"company\": \"\",\n    \"street\": \"Keizersgracht\",\n    \"house_number\": \"12\",\n    \"postal_code\": \"1015 CX\",\n    \"city\": \"Amsterdam\",\n    \"country\": \"NL\"\n  },\n  \"lines\": [\n    {\n      \"product_slug\": \"welcome\",\n      \"variant_id\": \"0198a3c4-5e6f-7a8b-9c0d-1e2f3a4b5c6d\",\n      \"quantity\": 1,\n      \"personalization\": {\n        \"template_data\": \"{\\\"pages\\\":[…]}\",\n        \"files\": [\n          {\n            \"element_uuid\": \"4f6e2a10-8c1b-4d2e-9f3a-5b6c7d8e9f01\",\n            \"name\": \"cat.png\",\n            \"key\": \"incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb24\"\n          }\n        ],\n        \"preview_key\": \"incoming/019f7f9f-12c1-7f74-a9c6-539064ddeb25\"\n      }\n    }\n  ]\n}\n```\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `lines` | Yes | At least one line |\n| `lines[].product_slug`, `lines[].variant_id` | Yes | What is ordered: a product on sale and one of its variants |\n| `lines[].quantity` | No | A whole number from 1 to 10,000. Default 1 |\n| `lines[].personalization.template_data` | Yes | The filled-in design, as the JSON string the widget handed over |\n| `lines[].personalization.files` | No | The pictures of this line: `element_uuid` (the widget's `elementId`) and `name` as the widget gave them, and the `key` of the upload. A picture without `element_uuid` is refused |\n| `lines[].personalization.preview_key` | No | The key of an uploaded picture of the result. The back office shows it with the order |\n| `email` | No | The customer's address, kept on the order. It does not link the order to an account: that is the `X-Customer-Token` header, see below |\n| `note` | No | A note from the customer |\n| `shipping_method` | No | A `value` from [the delivery options](https://docs.typograph.nl/guides/webshop/catalogue#delivery-options); `standard` when absent or unknown |\n| `payment_method` | No | `ideal`, `invoice`, `card` or `direct_debit`. An unknown value is left off the order, not refused |\n| `payment_reference` | No | The bank for `ideal`, the purchase order number for `invoice`, at most 120 characters. Ignored for the other methods |\n| `shipping_address` | No | `name`, `company`, `street`, `house_number`, `postal_code`, `city`, `country` (default `NL`). The billing address is the same |\n\n**On a customer's account.** Send the customer's `X-Customer-Token` header (optional, next to\n`X-Shop-Key`) to place the order on their account, so it shows up under\n[`GET /v1/shop/users/me/orders`](https://docs.typograph.nl/guides/webshop/customers#order-history). Without a token, or with one\nthat has expired, it is a guest order. The order is never refused for it: a checkout must not\nfail because a session ran out halfway.\n\n**The shop does not take payment.** It records the method the customer chose and the\nreference, and the order starts as `new`. Whoever handles the orders in the back office\nmoves it on.\n\n**Prices come from the shop.** The order is charged at the variant's price and the delivery\noption's price, plus VAT over both. Anything price-shaped in the request is ignored.\n\n**All or nothing.** The shop checks every line and every upload before it stores anything. A\nrefused order leaves nothing behind, so it is safe to fix and send again.\n\n`201`, with the order and a `Location` header to read it back from:\n\n```json\n{\n  \"data\": {\n    \"number\": \"ST-YYYY-0042\",\n    \"status\": \"new\",\n    \"email\": \"emma@example.com\",\n    \"shipping_method\": \"Standard delivery, Thursday\",\n    \"payment_method\": \"On account\",\n    \"subtotal_cents\": 2450,\n    \"shipping_cents\": 0,\n    \"vat_cents\": 515,\n    \"total_cents\": 2965,\n    \"lines\": [\n      {\n        \"id\": \"019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11\",\n        \"product_name\": \"Welcome\",\n        \"variant_name\": \"25 cards\",\n        \"sku\": \"WEL-25\",\n        \"quantity\": 1,\n        \"unit_price_cents\": 2450,\n        \"personalization\": {\n          \"id\": \"019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f10\",\n          \"template_url\": \"https://shop-api.typograph.nl/personalizations/…\",\n          \"preview_image_url\": \"https://shop-api.typograph.nl/personalizations/…\"\n        },\n        \"print_file\": { \"status\": \"queued\", \"is_downloadable\": false }\n      }\n    ],\n    \"access_token\": \"…\"\n  }\n}\n```\n\nThe order number is two letters, the year and a count of at least four digits: `ST-YYYY-0042`, and `ST-YYYY-10000` after the 9,999th. `shipping_method` and\n`payment_method` are the readable names, in the request's language.\n\n**`access_token` is sent this once.** Nothing can ask the shop for it again, so keep it with\nthe order number, in your confirmation page or your own records.\n\n### Order errors\n\nAll `422`. Nothing of the order was stored.\n\n| `error` | Meaning |\n|---------|---------|\n| `unreadable` | The body, or a line in it, is not readable JSON |\n| `empty_order` | No lines |\n| `unknown_product` | No product on sale with this slug |\n| `unknown_variant` | The product has no variant with this id |\n| `invalid_quantity` | A `quantity` that is not a whole number from 1 to 10,000 |\n| `missing_personalization` | A line without `personalization.template_data` |\n| `unreadable_personalization` | `template_data` is not a JSON document, or a picture has no `element_uuid` |\n| `missing_upload` | A picture without a key, or a preview sent inline instead of as `preview_key`: upload it first |\n| `unknown_upload` | Nothing was uploaded under a key, or the upload has expired |\n| `upload_too_large` | One picture over 20 MB |\n| `order_too_large` | The pictures and previews of the order over 200 MB together |\n| `too_many_files` | More than 100 pictures and previews in one order |","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#read-an-order-back","section":"Read an order back","audience":"api","headings":[],"text":"```bash\ncurl \"https://shop-api.typograph.nl/v1/shop/orders/ST-YYYY-0042\" \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Order-Token: …\"\n```\n\nThe same order as above, without `access_token`. Order numbers count up and are meant to be\nread out over the phone, so the number alone proves nothing: the token does. A wrong token\nanswers `404`, exactly like a number that does not exist.\n\nSend the token in the `X-Order-Token` header, from the browser or from your own server. Keep\nit out of addresses: a URL ends up in browser history, server logs and `Referer` headers, and\nthe token with it. When your own pages need it after a page load, keep it the way a session\nis kept, for instance in an `HttpOnly` cookie. `?token=` is still read but deprecated.\n\n| `status` | Meaning |\n|----------|---------|\n| `new` | Placed |\n| `paid` | Paid |\n| `in_production` | Being made |\n| `shipped` | Sent |\n| `cancelled` | Cancelled |\n\nThe status changes only when someone moves the order on in the back office.","agent_tools":[]},{"title":"Personalise and Order","url":"https://docs.typograph.nl/guides/webshop/orders#the-print-file-of-a-line","section":"The print file of a line","audience":"api","headings":[],"text":"Every personalised line gets a print-ready PDF: the file the printer gets, in CMYK with its\nbleed. `print_file.status` on each line says how it is doing; a line without a design has\n`print_file: null`.\n\n| `print_file.status` | Meaning |\n|---------------------|---------|\n| `queued` | Waiting its turn |\n| `rendering` | Being made, usually within a few minutes |\n| `ready` | Ready to download, see below |\n| `failed` | Making it went wrong. The shop has been told and sees it in its back office; it can make it again |\n\nRead the order again to follow it. `print_file.is_downloadable` says whether the PDF is handed out\nnow. That depends on one switch in the shop's back office,\n[**Customers may download their print files before the order is paid**](https://docs.typograph.nl/shop/settings/print-files)\nunder Settings → Print files. It is **on by default**: `is_downloadable` is `true` as soon as the\nfile is `ready`. A shop that turns it off hands the PDF out once the order is `paid` (or\n`in_production`, `shipped`); until then a ready file has `is_downloadable: false`. Ask for the address of a downloadable PDF with the\norder's token, as for the order itself:\n\n```bash\ncurl \"https://shop-api.typograph.nl/v1/shop/orders/ST-YYYY-0042/lines/019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11/print-file\" \\\n  -H \"X-Shop-Key: shop_pk_…\" \\\n  -H \"X-Order-Token: …\"\n```\n\n```json\n{\n  \"data\": {\n    \"url\": \"https://…/print-files/ST-YYYY-0042/019f7fa0-3b2c-7d1e-8f4a-6b5c4d3e2f11.pdf?…\",\n    \"expires_at\": \"YYYY-MM-DDT14:10:00+02:00\"\n  }\n}\n```\n\nThe address works for five minutes. A link or a navigation cannot carry the shop key or the\ntoken, so fetch the address when somebody clicks, from the page or from your own server, then\nsend the browser to `url`. The storefront does this from its server. Ask again for a new one\nrather than keeping it. A cancelled order shows no print file and hands none out.\n\n| Status | `error` | Meaning |\n|--------|---------|---------|\n| `404` | `not_found` | No such order, a wrong token, or no line with this id in the order |\n| `409` | `print_file_not_ready` | The line has no PDF yet, or making it failed |\n| `409` | `awaiting_payment` | The shop hands the PDF out once the order is paid |\n| `409` | `order_cancelled` | The order was cancelled |","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview","section":null,"audience":"api","headings":[],"text":"A Typograph webshop sells personalised print products: a visitor fills in a design with the\n[Form Widget](https://docs.typograph.nl/guides/integration/form), and orders it as a printed product. The shop itself —\nits catalogue, its orders and its [back office](https://docs.typograph.nl/shop/) — runs as its own service, and every\nstorefront reaches it over one API, `/v1/shop`.\n\nThe storefront is yours to build. Our own demo storefront, Stamped at\n[shop.typograph.nl](https://shop.typograph.nl), uses exactly the calls on these pages and\nnothing else, so whatever it can do, yours can too.\n\n| Part | What it is | Page |\n|------|-----------|------|\n| Catalogue | Products, categories, filters, search, delivery options | [Catalogue](https://docs.typograph.nl/guides/webshop/catalogue) |\n| Personalisation and orders | The form widget, picture uploads, placing an order and reading it back | [Personalise and Order](https://docs.typograph.nl/guides/webshop/orders) |\n| Customers | Optional accounts, a shortlist and order history | [Customers](https://docs.typograph.nl/guides/webshop/customers) |\n\nThe full specification is in the [Webshop API Reference](https://docs.typograph.nl/webshop-api-reference).","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview#not-the-platform-api","section":"Not the platform API","audience":"api","headings":[],"text":"The webshop is a separate service from the [Typograph API](https://docs.typograph.nl/guides/api/overview). It does not\nsit behind `api.typograph.nl`, it takes no OAuth token, and the gateway's rate limits and\nscopes do not apply to it.\n\n**Each shop has its own address.** The examples on these pages use the demo shop:\n\n```\nhttps://shop-api.typograph.nl\n```\n\nReplace that with your own shop's address. The paths are the same for every shop.","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview#authentication","section":"Authentication","audience":"api","headings":["What a browser may send","The product documents","Picture uploads"],"text":"Every call carries the shop's **publishable key** in `X-Shop-Key`:\n\n```bash\ncurl https://shop-api.typograph.nl/v1/shop/products \\\n  -H \"X-Shop-Key: shop_pk_…\"\n```\n\nThe shop owner finds the key in the back office under **Settings → Connection**, in the\n**Shop key** card (see [Connection](https://docs.typograph.nl/shop/settings/connection)).\n\n**Publishable means it belongs in your page.** The visitor's own browser places the order, so\nthe key has to be readable there. It names the shop; it does not prove who is asking.\n\nWhat stops someone from copying the key into their own site is the **Allowed websites** list\nunder it. A browser always says which site a request comes from (the `Origin` header) and\ncannot lie about it, so the shop answers only the sites on that list:\n\n| Request | Answer |\n|---------|--------|\n| No `X-Shop-Key`, or the wrong one | `401` |\n| Right key, from a site on the list | Served, with `Access-Control-Allow-Origin` for that site |\n| Right key, from a site **not** on the list | `401` |\n| Right key, no `Origin` at all (your own server) | Served |\n\nThe list is empty when a shop starts, which means no website may use the key yet. Add every\norigin your storefront runs on, one per line, as scheme and host without a path:\n`https://kaarten.nl`, and `https://www.kaarten.nl` separately if you use both.\n\nCreating a new key in the back office stops the old one at once: every site that uses it gets\n`401` until it has the new one.\n\n### What a browser may send\n\nA page on a site on the list may make every call from the browser. The shop answers\ncross-origin `GET`, `POST`, `PUT` and `DELETE` requests with the headers `Content-Type`,\n`X-Shop-Key`, `X-Customer-Token`, `X-Order-Token` and `Accept-Language`, and lets the browser\nread the `Location` and `Retry-After` headers of a response. That covers the catalogue, the\npicture uploads, placing and reading back an order, and the\n[customer calls](https://docs.typograph.nl/guides/webshop/customers).\n\nYour own server may make the same calls; without an `Origin` the list does not apply.\n\n### The product documents\n\nThe form widget loads a product's form, template, manifest, fonts and pictures from the shop\nitself (see [Personalise and Order](https://docs.typograph.nl/guides/webshop/orders#1-show-the-form)). Those addresses take no key,\nbut they answer the same list: a page on a site that is not on it gets `403`. A request\nwithout `Origin`, such as a plain `` or your own server, is served.\n\n### Picture uploads\n\nAt checkout, the visitor's browser uploads pictures straight to the shop's file storage, not\nto the API. That storage keeps its own list of sites that may upload. **Ask\n[Typograph support](https://docs.typograph.nl/guides/support) to add your storefront's origin** before you\ngo live; until then the uploads from your site are refused by the browser.","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview#response-shapes","section":"Response shapes","audience":"api","headings":[],"text":"The shapes are the ones the platform's own services use.\n\nA single resource sits under `data`:\n\n```json\n{\n  \"data\": { \"slug\": \"welcome\", \"name\": \"Welcome\" }\n}\n```\n\nA collection is `data` plus a `meta`:\n\n```json\n{\n  \"data\": [{ \"slug\": \"birth\", \"name\": \"Birth\" }],\n  \"meta\": { \"total\": 1, \"limit\": 100, \"offset\": 0, \"has_more\": false }\n}\n```\n\n- **Paging.** The product listing and search take `limit` and `offset`: `limit` is 25 when\n  you send none, and at most 100. Categories and delivery options come back whole; a\n  customer's order history comes back in one list of at most 100 orders.\n- **Prices** are whole cents, excluding VAT: `2450` is € 24,50. The shop adds VAT at checkout,\n  over the goods and the delivery together.\n- **JSON** is snake_case.\n- **Language.** Send `Accept-Language` with the visitor's language. The shop's own words —\n  product, variant, category and filter names, badges and descriptions — come back in any\n  language the shop lists at `GET /v1/shop/languages` and has translated; a text that is not\n  translated yet comes in the shop's main language. Texts the shop writes for people — the\n  names of the sort orders and the delivery options, and most error descriptions — come in\n  `en` or `nl`; anything else gets English. Filter values in a request stay in the main\n  language, whatever language the labels are shown in.","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview#errors","section":"Errors","audience":"api","headings":[],"text":"A refusal has the same shape as on the platform:\n\n```json\n{\n  \"error\": \"not_found\",\n  \"error_description\": \"This product does not exist.\"\n}\n```\n\nMatch on `error`, which does not change. `error_description` is written to be shown to a\nvisitor as it is.\n\n| Status | `error` | When |\n|--------|---------|------|\n| `401` | `unauthorized` | No key, the wrong key, or a site not on the allowed list |\n| `401` | `not_signed_in` | A customer call without a valid `X-Customer-Token` |\n| `404` | `not_found`, or the code its page names | An unknown product, category or order, or an order read with the wrong token. A product saved to the shortlist that does not exist answers `unknown_product` (see Customers) |\n| `422` | see the page of the call | The request was understood and refused; nothing was stored |\n| `429` | `too_many_requests` | Too many uploads or feedback messages from one visitor, or too many sign-in attempts for one e-mail address, see below |\n\nThe `422` codes are listed with the call that returns them: [uploads](https://docs.typograph.nl/guides/webshop/orders#upload-errors),\n[orders](https://docs.typograph.nl/guides/webshop/orders#order-errors) and [customers](https://docs.typograph.nl/guides/webshop/customers#errors).","agent_tools":[]},{"title":"Webshop API","url":"https://docs.typograph.nl/guides/webshop/overview#rate-limits","section":"Rate limits","audience":"api","headings":[],"text":"Three calls are limited. Over a limit they answer `429` `too_many_requests` with a `Retry-After`\nheader in seconds.\n\n| Call | Limit | Counted per |\n|------|-------|-------------|\n| `POST /v1/shop/uploads` | 30 requests per hour | Visitor, by IP address |\n| `POST /v1/shop/sessions` | 5 attempts per 15 minutes | E-mail address, known or not; a successful sign-in clears it |\n| `POST /v1/shop/feedback` | 10 per hour | Visitor, by IP address |\n\nA checkout normally asks for uploads once. Sign-in is counted per e-mail address rather than per\nvisitor because a storefront that signs customers in from its own server would otherwise make\nthem all share one budget. The price is that anyone who knows an address can keep its owner out\nfor a while by spending the five tries.\n\nThe other calls have no limit of the shop's own.","agent_tools":[]}]}