---
name: filemonk-api
description: Manage a Shopify store's Filemonk data (digital product delivery) through Filemonk API v2 - orders and their downloads, download emails, files, digital products, licence keys, customers, download logs, email delivery and settings. Use when a merchant asks about or wants to change anything Filemonk delivers.
---

# Filemonk API v2 — Agent Skill

You are integrating with the Filemonk API v2 — digital product delivery for Shopify: orders and their downloads, files, digital products, licence keys, customers, settings and the account. This is the root skill document: read it to learn how to authenticate, discover the surface, and call any action.

## Start here

- **Base URL:** `https://app.filemonk.io/api/v2/external` (production).
- **Auth:** send the key as `X-Auth-Token: <api_key>` (`X-API-Key` and `Authorization: Bearer <api_key>` are also accepted). The merchant creates it in Filemonk → Settings → Other → API keys (shown once; each key expires, and a `401` with `code: expired_api_key` or `revoked_api_key` means it needs replacing).
- **Every action:** `GET https://app.filemonk.io/api/v2/external/help` and the public `https://app.filemonk.io/api/v2/external/openapi.json` list every action with its parameters and an example. Read one of them before guessing a path.
- **Rate limit:** 40 points per 60 seconds per API key (a read costs 1, a write 2), shared by everything using the key. A 429 did nothing: wait the `Retry-After` seconds (`retry_after` in the body), then send the same request again.
- **Batch:** Use one list call with filters and `per_page` (up to 200) instead of one read per record, and a bulk action (`POST /licences/bulk_delete`, `POST /assets/bulk_delete`) instead of looping single writes. About 40 calls a minute is the whole budget.
- **Safety:** Confirm with the merchant before deletes, bulk emails and anything that charges: say what will change and how many records it touches, and write only after they agree. Actions that take `confirm` preview first: send them without it and nothing is deleted; the 409 `confirmation_required` lists the counts in `failure_data`. Tell the merchant, ask, and resend with `confirm: true` only after they agree. Put `confirm` in the JSON body of a POST, PATCH or PUT and in the query string of a DELETE (`DELETE /assets/:id?confirm=true`); the actions table marks every action that takes it.
- **Connect an AI assistant ("connect my store"):** add `https://app.filemonk.io/api/v2/external/mcp` as a custom connector, then sign in with the store's myshopify.com address or the connector key. Steps under Recipes.

## Recipes

Paths below are relative to the base URL.

- **Set up download emails:** `GET /settings` and check `email.enabled`; then `GET /digital_products` and check each product's `files_count`. A product with no files delivers nothing. Turn email on with `PATCH /settings` `{"email": {"enabled": true}}` after the merchant agrees, and `POST /settings/test_email` to see it.
- **Why didn't my customer get their file:** start with `GET /orders/:id/troubleshoot`: it checks Shopify, the stored order, the product files, order conditions, access, cancellation and whether a download email went out, and ends with a `diagnosis` naming the request that fixes each finding. Then `GET /emails?order_id=<order>` and read each email's `status` (bounced, rejected or complained means the address failed; no row means no email was sent), `GET /orders/:id/downloads` for the files and `GET /download_logs?order_id=<order>` to see whether they downloaded anyway. Resend with `POST /orders/:id/send_email` only after the merchant agrees.
- **How many orders, and the latest one:** `GET /orders?per_page=1` and read `meta.total_count`. The list is newest first by `ordered_at` (when the buyer ordered); `sort=created_at desc` orders by when Filemonk recorded it.
- **Change a customer-facing text:** for a download page heading or intro or an email subject, `GET /settings/texts?locale=de` to read the texts, then `PATCH /settings/texts` with `{"locale": "de", "texts": {"download_page": {"texts": {"thanks": "Thanks for your purchase!"}}}}`; fields you leave out are untouched. The download page's heading or intro line is `download_page.texts.thanks`; there is no `heading` key. Email wording is under `email.*` (subject, body); read the tree first and use the key whose current text matches what the merchant describes.
- **How many licence keys are left:** `GET /licence_tags` and read `totals.pending` (shop-wide, across every tag; filters and pages don't change it). Per tag, the keys left are that tag's `licences.pending`. `total` includes keys already assigned or used, so it is not the number left.
- **Revoke an order:** `PATCH /orders/:id` with `{"access": {"revoked": true}}` (`false` restores it). There is no revoke POST action.
- **Change the download limit:** `PATCH /settings` with `{"download_limits": {"per_file_count": 3, "days": 30}}`. `per_file_count` is downloads allowed per file, `days` is days after the order the files stay available, and `null` means unlimited. `show` only controls whether the download page displays the limits. Give one buyer more downloads with `POST /orders/:id/reset_downloads`.
- **Connect an AI assistant (MCP):**
  - MCP server URL: https://app.filemonk.io/api/v2/external/mcp (the merchant can also copy it from Filemonk → Settings → Other → AI connectors).
  - Claude or ChatGPT: add that URL as a custom connector. It opens a Filemonk sign-in page: enter the store's myshopify.com address, then approve the connection in the Shopify admin.
  - Connector key instead of the store address: on that page choose "Use a connector key instead" and paste the connector key from Filemonk → Settings → Other → AI connectors (it is not the API key, and sent as a header it is rejected with 401).
  - Script with a connector key (no browser): `POST https://app.filemonk.io/api/v2/external/oauth/register` with {"client_name", "redirect_uris": ["http://127.0.0.1/cb"], "token_endpoint_auth_method": "none"} gives a `client_id`; `POST https://app.filemonk.io/api/v2/external/oauth/authorize` as a form with `response_type=code`, `client_id`, `redirect_uri`, `code_challenge` (S256), `code_challenge_method=S256`, `resource=https://app.filemonk.io/api/v2/external/mcp` and `mcp_connector_key` answers 302 with `Location: <redirect_uri>?code=…`; `POST https://app.filemonk.io/api/v2/external/oauth/token` with `grant_type=authorization_code`, `code`, `client_id`, `redirect_uri` and `code_verifier` returns an `access_token` to send to /mcp as `Authorization: Bearer`. No page scraping is needed.
  - Claude Code, Cursor or a script: send an API key (Filemonk → Settings → Other → API keys) as a header, e.g. `claude mcp add --transport http filemonk https://app.filemonk.io/api/v2/external/mcp --header "X-Auth-Token: <api_key>"`.
  - Never ask the merchant to paste a key into the chat. An agent that already has the API key is connected: GET /account shows which store.

## Getting started

- **Base URL:** `https://app.filemonk.io/api/v2/external`
- **Auth:** every call endpoint needs an `X-Auth-Token: <api_key>` header (`X-API-Key: <api_key>` and `Authorization: Bearer <api_key>` are also accepted). The merchant creates the key in the Filemonk dashboard → Settings → Other → API keys; it is shown once and expires (30 to 365 days). API v1 takes the same keys. A `401` with `code: expired_api_key` or `revoked_api_key` means the merchant has to create or rotate a key; a `Deprecation` header or a `warnings` entry means the store's old non-expiring key is in use and should be replaced. This document, `llms.txt` and `openapi.json` are public so you can learn the API before you have a key.
- **Content type:** JSON in, JSON out. Errors return `{ "errors": [string, ...] }`, plus `code` when there is a machine-readable reason (see Errors).
- **Reference:** `https://app.filemonk.io/api/v2/external/openapi.json` is the OpenAPI 3.1 document for everything below, request schemas included.
- **MCP:** prefer tool calling? `POST https://app.filemonk.io/api/v2/external/mcp` is an MCP server (Streamable HTTP) with three tools — `search` finds the action for a request, `execute_read` runs a GET action, `execute_write` runs a POST, PUT, PATCH or DELETE action — over the same actions as the REST API. Auth: the same API key, or OAuth. The connector manifest is `https://app.filemonk.io/api/v2/external/mcp.json`.

## Core idea

The API is action-driven, not endpoint-driven. Every action declares a description, aliases (natural-language phrasings), a URL path, and a verb. To handle a request: scan the alias arrays in the help manifest, match the merchant's intent to a single action, build its request body from the schema, and call it.

## Discovery

`/help` and the public `openapi.json` both list every action with its request schema. All GET:

- `GET https://app.filemonk.io/api/v2/external/help` — every registered action: path, method, description, aliases, request schema.
- `GET https://app.filemonk.io/api/v2/external/help?prefix=/orders` (or `/assets`, `/digital_products`, or any deeper scope) — scoped manifest.
- `GET https://app.filemonk.io/api/v2/external/<scope>/help` — same, reachable at any URL level.

The manifest is generated from the live action registry; it cannot drift from what the dispatcher accepts.

## Conventions

- **Named actions carry side effects; PATCH carries state.** Sending the download email, resetting download counts, attaching files and other transitions are named POST actions. State and toggles are set via a resource's deep-partial `PATCH` (fields you don't send are untouched): revoking or restoring an order's access is `PATCH /orders/:id` with `{"access": {"revoked": true}}`, and download limits are `PATCH /settings` `download_limits`.
- **Download limits** (`GET`/`PATCH /settings`, section `download_limits`): `per_file_count` is how many times each file can be downloaded per order, `days` is how many days after the order the files stay available, `null` on either means unlimited. `show` only decides whether the download page displays those limits to the buyer; it does not turn them on or off. `messages` holds the texts the page shows.
- **Reads return the resource; writes answer with what they did.** `200` for a change that completed in the request (body: the updated resource), `201` for a create (body: the new resource), `202` only when the change was saved and the rest of the work — emails, Shopify sync, a bulk job — continues in the background: the body is the resource snapshot plus `queued: true`. There is no job to poll; read the resource again later if you need the outcome.
- **Order ids:** An order `:id` is the order name (`#1042`), the Shopify order gid or id, or Filemonk's order id. Write the `#` as `%23` in a URL path (`/orders/%231042/troubleshoot`).
- **Ids:** Filemonk ids are UUIDs. Shopify ids are accepted as `gid://shopify/Product/123` or the plain number `123` in request fields named `shopify_*_id`, and are always returned as gids (`"shopify_product_id": "gid://shopify/Product/123"`). Compare gids to gids.
- **Only declared parameters.** Each action takes exactly the fields in its request schema; an unknown key is a `422` with `code: "unknown_parameters"`, `unknown` (what you sent) and `accepted` (what it takes). Fix the call instead of retrying.
- **Failures return `{ "errors": [...] }`** with a status. `409` means the request conflicts with the current state — read the error, don't retry blindly.

## Working safely

- Read before you write: fetch the order, product or setting first, tell the merchant what will change, and write only once they confirm.
- Confirm with the merchant before deletes, bulk emails and anything that charges: say what will change and how many records it touches, and write only after they agree. Actions that take `confirm` preview first: send them without it and nothing is deleted; the 409 `confirmation_required` lists the counts in `failure_data`. Tell the merchant, ask, and resend with `confirm: true` only after they agree. Put `confirm` in the JSON body of a POST, PATCH or PUT and in the query string of a DELETE (`DELETE /assets/:id?confirm=true`); the actions table marks every action that takes it.
- Deletes of files, digital products, licence keys and licence tags are permanent; there is no undo. A deleted order stops its download page and only comes back by importing it again (`POST /orders`).
- Sends are real: `send_email`, `notify_buyers`, `send_emails` and `test_email` email buyers or the merchant, and `fulfil` fulfils the order in Shopify.
- Never ask the merchant to paste the API key into a chat.
- Every action has a request and response example in `openapi.json`; copy the field names from there instead of guessing.

## The Filemonk assistant and Shopify Sidekick

- **The Filemonk support assistant** is the AI chat inside the Filemonk app. It answers from the merchant's own data and can make changes after a preview the merchant confirms. Try: "Why didn't order #1042 get its files?", "How many orders did we get this month?", "Resend the download email for #1042", "Which products have no files?", "Change the download page's thanks text in German to ...".
- **Shopify Sidekick** reaches the same data through the Filemonk app's tools in the Shopify admin (it appears when the merchant opens Sidekick inside the store admin and asks about Filemonk). Try: "Show the latest Filemonk orders", "Check why #1042 has no download", "Is my Filemonk email domain verified?", "How many downloads did Filemonk record this month?".
- Both sign in as the merchant inside Shopify, so an outside agent cannot drive them with the API key; use this API instead. Neither can do anything the API cannot, and both ask for confirmation before deletes and sends.

## Pagination

Lists take `page` (1-based) and `per_page` (default 50, max 200) and answer `{ "data": [...], "meta": { "total_count", "page", "per_page", "total_pages" } }`. Keep requesting `page + 1` until `page >= total_pages`. Lists are newest first unless the action documents a `sort` (`GET /orders` is newest by `ordered_at`).

## All registered actions

Every action live on the API right now, with method, path, description, and aliases.

| Method + Path | Description | Aliases | Confirm |
|---|---|---|---|
| `GET /account` | The shop this API key belongs to: its domain, name, contact email, currency and time zone, the Filemonk plan it is on with the plan's limits (orders per cycle, file count, storage and bandwidth, in bytes), and the current billing cycle's usage (bytes uploaded, bytes downloaded, paid orders). Read-only; nothing about charges or money. | my account, my plan, which plan am I on, usage, how much storage have I used, bandwidth used, orders this month, shop details, whose key is this |  |
| `GET /analytics` | The home page counters: orders delivered and files downloaded, for all time or this calendar month (period=all_time\|this_month). | how many orders this month, download count, store stats, analytics |  |
| `POST /assets` | Add a file to the library, as the Files page's Add does. type is file (a stored upload), link (an absolute url, such as https://…, to an external page or download) or licence_key (a pool of keys). A file answers with upload: {url, method, headers, expires_at}; PUT the file's bytes there within the hour, nothing else is needed. An AI assistant that can't send the bytes itself (in a chat) should ask the merchant to upload the file on the Files page instead. watermark.enabled applies to PDFs; licence_keys.autogenerate mints keys on demand, licence_keys.tags names the pools the file draws from. | upload a file, add a file, add a link, create a licence key file, new file, add a download |  |
| `GET /assets` | List the shop's files, links and licence-key assets alphabetically, paginated (page, per_page up to 200). type narrows to file, link or licence_key; search matches the file name the way the Files page search does; licence_tag narrows licence-key assets to one tag. Each row carries how many digital products and orders use it. | list files, uploaded files, find a file, which files are unused, licence key files |  |
| `DELETE /assets/:id` | Delete a file from the library, as the Files page's Delete does: it leaves every digital product and order it was on, and the stored body is purged. | delete file, remove file, delete this upload, remove the link | `confirm` in the query |
| `GET /assets/:id` | One file in full: name, size and type, a one-hour signed download url for stored files (url_expires_at says when it stops working), watermark and licence-key settings, the digital products it belongs to (with folder) and the last five orders that received it. | show file, file details, download a file, which products use this file, who got this file |  |
| `PATCH /assets/:id` | Change a link's or licence key's name, a link's url, watermark.enabled or licence_keys (autogenerate, tags), as the Files page's edit does. Deep partial: only the fields sent change. A stored file's name is its filename: rename it by replacing the file with POST /assets/:id/replace. | turn on watermark for a file, change licence key tags, edit file, update the link url, set file to autogenerate keys |  |
| `GET /assets/:id/linked_orders` | Which orders and digital products a file is on, as the Files page checks before Delete: the orders that can download it now (newest first, up to 50) and the digital products that deliver it. | is this file on any orders, who has this file, linked orders for file, check before deleting file |  |
| `POST /assets/:id/replace` | Swap the stored body of a file for a new version, keeping its id, products and orders, as the Files page's Replace does. Answers with upload: {url, method, headers, expires_at}; PUT the new body there within the hour. | replace file, upload a new version, swap the file, update the pdf |  |
| `POST /assets/bulk_delete` | Delete several files at once (ids), as the Files page's bulk Delete does. Answers with the ids deleted and the ids that matched nothing. | delete these files, bulk delete files, remove several files | `confirm` in the body |
| `GET /customers` | List customers Filemonk has delivered to, newest first, paginated (page, per_page up to 200), each with how many orders they have. search matches email, phone or name; email matches exactly; unsubscribed=true narrows to buyers who opted out of the merchant's emails. | list customers, find a customer by email, unsubscribed customers, repeat buyers |  |
| `GET /customers/:id` | One customer by Filemonk id or Shopify Customer gid or id: contact details, order count and whether they unsubscribed from the merchant's emails. | show customer, look up customer, has this customer unsubscribed |  |
| `PATCH /customers/:id` | Opt a customer out of, or back into, the merchant's emails (marketing.unsubscribed), as the unsubscribe link in those emails does. :id is Filemonk's id or a Shopify Customer gid or id. | unsubscribe customer, resubscribe customer, stop emailing this customer, opt customer back in |  |
| `GET /customers/:id/orders` | The orders Filemonk delivered for one customer, newest first, paginated, as order summaries. | orders of a customer, what has this customer bought, customer purchase history |  |
| `POST /digital_products` | Make a Shopify product or one of its variants a digital product, as the Products page's Add does: give shopify_product_id (whole product) or shopify_variant_id (one variant; add shopify_product_id too unless Filemonk already knows the variant), the files to deliver as files: [{id, folder}], and optionally delivery.message, delivery.attach_to_every_order, preorder {enabled, launch_at, email {subject, heading, description}} (enabling a pre-order needs launch_at and delivery.message) and update_email {subject, body}. | add digital product, attach files to a product, make this product digital, set up delivery for a variant, new digital product |  |
| `GET /digital_products` | List the digital products (Shopify products or variants with files attached in Filemonk), by product title, paginated (page, per_page up to 200). search matches title, variant title or SKU the way the Products page search does; scope is product or variant; shopify_product_id / shopify_variant_id narrow to one Shopify product or variant; preorder and attach_to_every_order take true or false. | list digital products, which products deliver files, products with pre-orders, find the digital product for a variant, products attached to every order |  |
| `DELETE /digital_products/:id` | Stop delivering files for this product or variant, as the Products page's Delete does. The files stay in the library; the Shopify product is untouched. | delete digital product, remove files from product, stop delivering this product | `confirm` in the query |
| `GET /digital_products/:id` | One digital product in full: the Shopify product or variant it belongs to, every file with its folder, delivery message, pre-order and update-email settings, and how many orders received it. :id is Filemonk's id or a Shopify ProductVariant / Product gid or id. | show digital product, files attached to a product, pre-order settings of a product, what does this variant deliver |  |
| `PATCH /digital_products/:id` | Change a digital product's files or settings, as the product page does. Deep partial: delivery.message, delivery.attach_to_every_order, preorder.*, update_email.* change only when sent; files: [{id, folder}] replaces the whole file list and folders (add or remove single files with POST /digital_products/:id/files/attach and /detach instead). Enabling a pre-order needs preorder.launch_at and a delivery.message, sent now or already saved. | edit digital product, change delivery message, enable pre-order, set launch date, move file into folder, update the product's files |  |
| `POST /digital_products/:id/files/attach` | Add files (asset_ids) to a digital product, optionally inside a folder on the download page, as the product page's Add files does. Files already on it are left as they are. | attach file to product, add a file to this product, put files in a folder, link upload to variant |  |
| `POST /digital_products/:id/files/detach` | Remove files (asset_ids) from a digital product, as the product page's Remove does. The files stay in the library and on any other product. | detach file from product, remove a file from this product, unlink upload | `confirm` in the body |
| `POST /digital_products/:id/notify_buyers` | Email everyone who bought this product that it was updated, with a link to their download page, as the product page's Notify customers does. subject and body default to the product's update_email; save_as_default stores the ones sent for next time. | notify buyers, tell customers the file changed, send product update email, email everyone who bought this |  |
| `POST /digital_products/:id/refresh` | Ask Shopify for the product's current title, variant title and sku and store them, as opening the product page does. The answer is the digital product with exists_on_shopify filled in: false when Shopify no longer has the product or variant. | refresh product from shopify, is this product still on shopify, update product title, check deleted product |  |
| `GET /digital_products/deleted_on_shopify` | Check a page of digital products against Shopify, as the Products page does when it labels products deleted on Shopify: for each, whether its Shopify product or variant still exists. Same order and paging as GET /digital_products (page, per_page up to 200). | products deleted on shopify, stale digital products, which products no longer exist, clean up deleted products |  |
| `GET /digital_products/lookup` | The digital product a Shopify variant would be delivered from: its own variant-level product if one exists, else the product-level one. Takes shopify_variant_id and/or shopify_product_id (gid or numeric). 404 when neither has files configured. | what will this variant deliver, is this product digital, digital product for a Shopify variant |  |
| `GET /download_logs` | Every download across the shop, newest first, paginated (page, per_page up to 200). order_id (Filemonk id, Shopify order gid or id, or #name) and asset_id narrow to one order or file; from / to are ISO 8601 bounds on when the download happened. | download history, who downloaded what today, downloads of a file, download logs |  |
| `GET /emails` | Every email Filemonk sent, newest first, paginated (page, per_page up to 200), with the furthest point each reached. order_id (Filemonk id, Shopify order gid or id, or #name) narrows to one order; kind is delivery, product_update, product_update_test or preorder_release; delivery_status is queued, sent, delivered, opened, clicked, rejected, bounced or complained; from / to are ISO 8601 bounds on when it was sent. | bounced emails, emails sent today, did the download emails go out, email delivery report |  |
| `POST /exports` | Email the shop a CSV export (type: orders, assets, digital_products or licences; licences take an optional tag), as each page's Export CSV does. The file goes to the shop's email address when it is ready; nothing is returned inline. | export orders to csv, download all my files as a spreadsheet, export licence keys, csv of digital products, email me an export |  |
| `POST /imports` | Start a bulk import of historic orders from CSV text (csv), as the bulk import page does. Columns are the page's template (sku, filename, external_url, data_type, link_to_product, custom_product) in that order, or column_mapping {sku: 0, filename: 1, ...} gives the 0-based index of each. data_type is sku or variant_id (blank means sku); link_to_product and custom_product are yes, no or blank. Rows run in the background; follow progress with GET /imports/:id. | import orders from csv, bulk import, upload a csv of orders, start an import |  |
| `GET /imports` | CSV order imports newest first, paginated, each with its row counts (total, completed, failed) and status: processing until every row is handled, then completed, or failed when no row went through. | list imports, did my import finish, import history, how many rows failed |  |
| `GET /imports/:id` | One CSV import: status, row counts and up to 50 failed rows with the reason each was rejected. | show import, why did these rows fail, import errors |  |
| `POST /imports/preview` | Check CSV text (csv) before importing it, as the bulk import page's preview and column mapping steps do: the headers, the first rows per column, a suggested column_mapping for POST /imports, and the same row checks the import runs (unknown files, SKUs or urls), without importing anything. | preview csv import, check my csv, validate import file, map csv columns |  |
| `GET /licence_tags` | The licence tags the shop's keys and licence-key files use, as the Licence tags page lists them: key counts by status and the digital products each tag supplies. No Tag (uploaded keys without a tag) and Autogenerated are built-in groups, marked virtual. search matches the name. | list licence tags, which tag is running out of keys, keys left per tag, licence key pools |  |
| `DELETE /licence_tags/:name` | Delete a licence tag with everything in it, as the Licence keys page's Delete tag does: its pending keys and the licence-key files tagged with it. | delete tag, remove licence tag and its keys, delete key pool | `confirm` in the query |
| `GET /licence_tags/:name` | One licence tag by name (or No Tag / Autogenerated): key counts by status and the digital products whose licence-key files draw from it. | show licence tag, how many keys are left for this tag, which products use this tag |  |
| `PATCH /licence_tags/:name` | Rename a licence tag (new_name), as the Licence keys page's Rename tag does: every key and licence-key file of this shop moves to the new name. | rename tag, rename licence tag, change tag name |  |
| `POST /licences` | Add licence keys to the pool (keys, one string each), under a tag when given, as the Licence keys page's Add keys does. Orders still waiting for a key from that pool are served right after. | add licence keys, upload keys, paste license keys, add keys to tag, top up the key pool |  |
| `GET /licences` | List licence keys newest first, paginated (page, per_page up to 200). key_status is pending, assigned or used; source is uploaded or auto_generated; tag narrows to one licence tag (No Tag and Autogenerated are the two built-in groups); search matches part of the key. | list licence keys, unused licence keys, how many keys are left, how many licence keys do I have, licence keys left, keys for a tag, find a key |  |
| `DELETE /licences/:id` | Delete one licence key, as the Licence keys page's Delete does. A key on an order is removed from it and the order is marked so no replacement is assigned automatically. | delete licence key, remove a key, delete this license key | `confirm` in the query |
| `GET /licences/:id` | One licence key by Filemonk id: the key, its status, tags, and the order and licence-key file it was assigned to. | show licence key, which order got this key |  |
| `PATCH /licences/:id` | Edit one licence key, as the Licence keys page's edit does: key (the text), key_status (pending, assigned, used) or tags (the whole list). Deep partial. | edit licence key, fix a typo in a key, mark key as used, retag one key, change key status |  |
| `POST /licences/bulk_delete` | Delete licence keys in bulk, as the Licence keys page does, one of three ways: ids (a list), {unused: true, tag} (every unassigned key of a tag; include_assigned: true takes the assigned ones too) or {duplicates: true, tag} (keep the oldest pending row per key text, drop the rest). | delete unused keys, delete duplicate keys, bulk delete licence keys, clear a key pool, remove these keys | `confirm` in the body |
| `POST /licences/check` | Before adding keys: which of these keys already exist under any of these tags. Answers {duplicates: [key, ...]}; nothing is written. | check for duplicate keys, do these keys already exist, validate keys before import |  |
| `POST /licences/import` | Import licence keys from CSV text (csv), as the Licence keys page's Import does. Columns are matched by header: key (Licence Key, License Key or Key), tag (Tag, Tags or Tags linked) and status. Only the key column is required; without a tag column the keys are imported untagged. column_mapping {key, tag, status} gives 0-based column indexes instead. Keys whose status column says assigned or used are skipped. | import keys from csv, upload a csv of licence keys, bulk import license keys |  |
| `GET /licences/lookup` | Find a licence key by its exact value (key=XXXX-YYYY): is it one of ours, has it been assigned, and to which order. 404 when the shop has no such key. | is this licence key valid, who was this key sent to, look up a key a customer sent |  |
| `POST /licences/move` | Move licence keys into a tag (to_tag), replacing the tags they had, as the Licence keys page's Move does: either ids (a list) or tag (every key of that tag, No Tag included). to_tag must exist already, or be No Tag to clear tags. | move keys to tag, retag licence keys, merge tags, untag keys |  |
| `POST /orders` | Import one order from Shopify by its order id or gid, as the Orders page's Find order does: Filemonk fetches it, links the digital products it contains and, unless send_email is false, sends the download email. Returns the order in full. | import order, add order from shopify, find order on shopify, process this order, deliver an order filemonk missed | `confirm` in the body |
| `GET /orders` | List the orders Filemonk delivers files for, newest first by ordered_at (when the buyer placed the order; sort=created_at desc orders by when Filemonk recorded it), paginated (page, per_page up to 200). meta.total_count is the number of orders. search matches the order name, email or phone the way the Orders page search does. financial_status filters by Shopify financial status; access by Filemonk's access state (active, revoked, partially_revoked, expired); email, customer_id, shopify_product_id and digital_product_id narrow to one buyer or one product; created_at_min / created_at_max (or from / to) are ISO 8601 bounds on when Filemonk recorded the order. Deleted orders are left out. | list orders, recent orders, orders for a customer, who bought this product, unpaid orders, orders with access revoked, search orders |  |
| `DELETE /orders/:id` | Delete an order from Filemonk, as the order page's Delete does: it leaves the lists and the buyer's download page stops working. The Shopify order is untouched. | delete order, remove order, hide this order | `confirm` in the query |
| `GET /orders/:id` | One order in full: the buyer, Shopify statuses, Filemonk's access state and expiry, delivery (download email, download page link), and every line item with its files, watermark state and licence keys. :id is Filemonk's order id, the Shopify order gid or id, or the order name (#1042). | show order, look up order, order details, find order by number, what did this customer get |  |
| `PATCH /orders/:id` | Revoke or restore the buyer's access to the whole order (access.revoked), as the order page's access toggle does. Restoring also clears a fraud flag. Per line item access is PATCH /orders/:id/line_items/:line_item_id; download counters are POST /orders/:id/reset_downloads. | revoke access, restore access, disable downloads for this order, re-enable the order, block this order, unblock order |  |
| `GET /orders/:id/download_logs` | Every download recorded for one order, newest first, paginated: which file, when, from which IP, country and browser. | download history of an order, did the customer download, who downloaded this order |  |
| `GET /orders/:id/downloads` | What the buyer of an order can download right now: one row per file with its download url (null while a watermark is still being applied, or when access is revoked, expired or the download limit is reached), the watermark state and the download count. A watermarked file's url is a signed link that stops working at url_expires_at; a plain file's url is a redirect that does not expire and a link is its own address, so both have url_expires_at null. | download links for an order, resend download link, order files, can the customer download |  |
| `GET /orders/:id/emails` | Every email Filemonk sent for one order, newest first, paginated, with the furthest point each reached: queued, sent, delivered, opened, clicked, rejected, bounced or complained. | was the download email delivered, email history of an order, did the customer open the email |  |
| `POST /orders/:id/files/attach` | Attach files to this one order only (asset_ids), as the order page's Add custom files does: they appear on the buyer's download page as a line item of their own, invisible to other buyers of the same products. | add a file to this order, attach custom file, give this customer an extra file, send a bonus file to one order |  |
| `POST /orders/:id/files/detach` | Remove files that were attached to this one order (asset_ids). Files that come from the order's digital products are not affected; revoke the line item for those. | remove custom file from order, detach file, take a bonus file off this order | `confirm` in the body |
| `POST /orders/:id/fulfil` | Mark the order's digital line items fulfilled in Shopify now, as the order page's Mark fulfilled does, whether or not automatic fulfilment is on in settings. | mark fulfilled, fulfil the digital items, fulfill order in shopify, close out the order |  |
| `PUT /orders/:id/licences/:licence_id` | Swap the licence key on the order, as the order page's Reassign does: the current key goes back to the pool and key (an unassigned key of this shop) takes its place. Without key the row is left open for the automatic assigner. | reassign licence key, swap the key, replace key on order, give the customer a different key |  |
| `DELETE /orders/:id/licences/:licence_id` | Take a licence key off the order, as the order page's Unassign does. key_status says what happens to the key: pending returns it to the pool (default), used burns it. The file row is marked so no replacement key is assigned automatically. | unassign licence key, remove key from order, take back the key, return key to pool | `confirm` in the query |
| `POST /orders/:id/licences/assign` | Place a licence key on this order by hand, as the order page's Assign key does: asset_id names the licence-key file, key a specific unassigned key, tag the pool to draw from (default: the file's own pool, as at checkout: its licence tag, or untagged keys when the file has no tag). send_email true emails the buyer afterwards. | assign licence key, give this order a key, add a license key to the order, customer needs a key, assign key from tag |  |
| `PATCH /orders/:id/line_items/:line_item_id` | Revoke or restore access to one digital product on the order (access.revoked), as the order page's per-product toggle does. :line_item_id is line_items[].id from GET /orders/:id. Restoring one product on a revoked order turns the order back on with its other products kept off. | revoke access to one product, restore one line item, block a file on this order, re-enable one product for the customer |  |
| `POST /orders/:id/reset_downloads` | Zero the download counters so the buyer can download again: every file on the order, or one file when file_id (line_items[].files[].id from GET /orders/:id) is given. Resetting the whole order also restarts the day limit from now. | reset download count, let the customer download again, give more downloads, customer hit the download limit, reset downloads for one file |  |
| `POST /orders/:id/resync` | Re-sync an order already in Filemonk from Shopify, as the Orders page's Find order does for a stored order: Filemonk fetches it again and links the files its products have now (for an order stored before its products had files). Unless send_email is false, the buyer is emailed about newly linked files. A deleted order is restored. Returns the order in full. | resync order, refresh order from shopify, relink files to order, re-import order, order has no files | `confirm` in the body |
| `POST /orders/:id/send_email` | Send the download email for this order again, to the buyer or to another address (to), as the order page's Send email does. | resend download email, send the download link again, customer did not get the email, email the files to another address, resend order email |  |
| `GET /orders/:id/troubleshoot` | Why a buyer got no files, as the Orders page's troubleshooter shows: whether Shopify has the order, whether Filemonk stored it, and for every Shopify line item whether its product or variant has files in Filemonk (with counts) or is a custom product. Ends with a diagnosis and the request that fixes each finding. :id is the order name (#1042), the Shopify order gid or id, or Filemonk's order id. | customer got no files, customer did not receive download, why was this order not delivered, troubleshoot order, order missing in filemonk, diagnose order |  |
| `POST /orders/send_emails` | Send the download email again for several orders at once, in the background, as the Orders page's bulk Send emails does. ids are order ids, Shopify order gids or ids, or order names. | resend emails to these orders, bulk resend download emails, email all these customers again |  |
| `GET /settings` | Every setting the dashboard's settings pages expose, grouped by page: general, email, download_page, download_limits, order_conditions, shopify_sync, watermark, customer_accounts and eu_withdrawal. Keys and tokens are never included. | show settings, is the download email enabled, how many days can customers download, what are the order conditions, watermark settings |  |
| `PATCH /settings` | Change settings, as the settings pages do: send any subset of the sections GET /settings returns (general, email, download_page, download_limits, order_conditions, shopify_sync, watermark, customer_accounts, eu_withdrawal) with only the fields to change. Answers with every setting after the save. | change settings, turn off the download email, set download limit, enable watermarking, change order tags, update email subject, set the download page colours |  |
| `GET /settings/checkout_profiles` | List the shop's checkout and accounts configurations (Shopify checkout profiles) and which one is published, as the order status page setup shows before the merchant adds Filemonk's block. Read-only: adding the block happens in Shopify's checkout editor, which an agent can't open. | theme configurations, checkout profiles, which checkout is published, order status page setup |  |
| `GET /settings/email_domain` | The custom sending domain taken from the reply-to address, whether its DKIM records are verified, and the DNS records still to add when they are not. Runs the same check as Settings > Email. | is my email domain verified, dkim records, dns records for sending email, email domain status |  |
| `POST /settings/test_email` | Send a preview of the download email to the merchant (to; default the shop's email), built from one of the shop's own orders, as the email settings page's Send test email does. | send test email, preview the download email, email me a test, check the email template |  |
| `GET /settings/texts` | Read the customer-facing texts in one locale, as the settings Translate view shows them: the download email, download page, download limit messages, order status and customer account blocks, and the EU withdrawal consent. locale defaults to the shop's default language. Values are what is stored for that locale only; untranslated lists the texts not translated yet (buyers see the default language there). | translations, read translated texts, what does the download page say in french, which texts are not translated, email text in german |  |
| `PATCH /settings/texts` | Change customer-facing texts in one locale, as the settings Translate view does: send locale and texts, a partial tree with the same paths GET /settings/texts returns (for example {"email": {"subject": "…"}, "download_page": {"texts": {"button": "…"}}}). Answers with that locale's texts after the save. | translate the download email, change french texts, set german download page text, fix a translation |  |
| `POST /settings/texts/setup` | Set up a language, as the settings Translate view's "Translate" does: fill every customer-facing text in locale with Filemonk's own translation (from_locale, default the same locale). Answers with that locale's texts after the fill. | set up french, translate everything to german, add a language, reset translations | `confirm` in the body |

## Coming from API v1

API v1 (`https://app.filemonk.io/api/v1/external`) stays supported with the same key. Each v1 endpoint and where it lands in v2:

| API v1 | API v2 | Status | Notes |
|---|---|---|---|
| `GET /orders` | `GET /orders` | live | same filters; `search`, `email`, `financial_status`, product and date filters are kept |
| `GET /orders/:id` | `GET /orders/:id` | live |  |
| `GET /orders/:id/downloads` | `GET /orders/:id/downloads` | live | signed URLs and watermark status, as in v1 |
| `POST /orders/:id/attach_assets` | `POST /orders/:id/files/attach` | live | body `asset_ids` |
| `DELETE /orders/:id/detach_assets` | `POST /orders/:id/files/detach` | live | a named action instead of a DELETE with a body |
| `POST /orders/:id/notify` | `POST /orders/:id/send_email` | live | sends the download email for the order |
| `GET /assets` | `GET /assets` | live |  |
| `GET /assets/:id` | `GET /assets/:id` | live |  |
| `POST /assets` | `POST /assets` | live | a stored file answers with `upload: { url, method, headers, expires_at }`; PUT the body there within the hour, nothing else is needed |
| `PATCH /assets/:id` | `PATCH /assets/:id` | live |  |
| `DELETE /assets/:id` | `DELETE /assets/:id` | live |  |
| `POST /assets/:id/replace` | `POST /assets/:id/replace` | live |  |
| `GET /digital_products` | `GET /digital_products` | live |  |
| `GET /digital_products/:id` | `GET /digital_products/:id` | live |  |
| `POST /digital_products` | `POST /digital_products` | live |  |
| `PATCH /digital_products/:id` | `PATCH /digital_products/:id` | live |  |
| `DELETE /digital_products/:id` | `DELETE /digital_products/:id` | live |  |
| `POST /digital_products/:id/notify_buyers` | `POST /digital_products/:id/notify_buyers` | live |  |

## Errors

Every failure is `{ "errors": [sentence, ...] }`. Some add machine-readable keys:

- `401` — missing or invalid API key
- `404` — the resource doesn't exist for this shop, or the action path is not registered
- `409` — the request conflicts with the current state; read the error message
- `422` — validation failed. Field-by-field validation adds `field_errors` (`{ "dotted.field": ["message"] }`); an unknown request key adds `code: "unknown_parameters"`, `unknown` and `accepted`
- `429` — rate limited and nothing was done; `code: "rate_limited"`, `retryable: true`, `next_step`, `retry_after` (seconds) and the `Retry-After` header. Wait `retry_after` seconds, then send the same request again
- `502` — Shopify (or the email service) did not complete the request; `code: "shopify_error"`. Nothing was changed; try again shortly

## Rate limits

Cost-weighted per API key, shared with API v1: a read costs 1 point, a write 2, 40 points per fixed 60-second window. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix time the window ends); a 429 adds `Retry-After` (seconds), `retry_after`, `retryable: true` and a `next_step` in the body. A 429 did nothing: wait `retry_after` seconds, then send the same request again. The budget is per key, so every tool or script using the key shares it; prefer one list call with `per_page` over many single reads.
