Digital, service and backorder
Products that are not boxes: digital files with signed 72-hour download links, services, and selling at zero stock with a promised date.
Written 2026-09-16. Not every product is a box. A store can sell a file, an hour of work, or something it has not made yet, and Formahand treats all three as ordinary products: same catalog, same checkout, same orders, same emails. What changes is what checkout asks for, what the buyer gets afterwards, and what the product page says.
1. The four kinds
Every product has a kind:
| Kind | Ships? | Checkout asks for | After payment |
|---|---|---|---|
physical (the default) | Yes | Address and a shipping rate | The merchant fulfils and adds tracking |
digital | No | Nothing but payment | Signed download links, 72 hours each |
service | No | Nothing but payment | Nothing to deliver; the merchant follows up |
gift_card | No | Nothing but payment | Credit on your own shop, sent by email; it never runs out of stock (payments.md § Gift cards) |
A product keeps its kind everywhere: the dashboard's product editor (Kind & availability), POST/PUT /api/products, the upsert_product tool, and the public read API.
{ "name": "Field notes (PDF)", "slug": "field-notes", "priceMinor": 1200, "currency": "USD",
"kind": "digital", "inventory": 0,
"backorder": { "allowed": false } }
A gift card product is the thing the Gift cards add-on sells: mark it this way and checkout asks for no address, the stock column is left alone — credit is not a thing on a shelf — and each paid line becomes a code the buyer can spend at your own checkout. Set a price per denomination, one product each, and tag them so the add-on knows which lines are gift cards.
A cart of only digital and service lines needs no address and no rate: the hosted checkout is created without shipping collection, and the cart page hides the "Ship to" estimate. One physical line anywhere in the cart brings both back, and that mixed order is shipped exactly as before — the digital lines simply also get their links.
Stock still applies to digital and service products. Leave inventory high (or allow backorders) if you do not want a file to sell out.
2. Backorder
A product may be sold while it is out of stock:
{ "backorder": { "allowed": true, "promisedAt": "2026-10-12",
"message": "Pre-order — the next run ships in October." } }
allowed is the switch. promisedAt is an ISO date (YYYY-MM-DD); without a message the storefront writes the sentence itself ("On backorder — ships by 12 October 2026."). The promise appears on the product page, on the cart line and in the order confirmation email, so nobody is surprised.
What changes mechanically: the stock refusal at checkout is skipped for that product, its inventory never goes below zero, and the order line is recorded with backordered: true (visible on the order in the dashboard and in get_order). Everything else — fulfilment, tracking, refunds — is unchanged.
3. Digital files
A digital product carries up to 10 files. Each file is stored with your store's own media, never anywhere a buyer can browse, and is described by { id, name, bytes, contentType }. Where the file physically sits stays inside your store: no API, tool or dashboard screen ever returns it.
- Allowed:
pdf,zip,epub,mp3,mp4,png,jpg,txt,csv,json. - Up to 200 MB per file. Bytes count towards the store's media allowance exactly like product images.
Upload from the dashboard (Products → a digital product → its Files part, #products/<id>/files), or by token:
# multipart, straight from a machine
curl -sS -X POST -H "Authorization: Bearer fh_<mode>_…" \
-F "file=@field-notes.pdf" \
"https://formahand.com/api/product-files?productId=<product id>"
# or import a public https file the platform streams in
curl -sS -X POST -H "Authorization: Bearer fh_<mode>_…" -H "Content-Type: application/json" \
-d '{"productId":"<product id>","url":"https://example.com/field-notes.pdf","name":"Field notes.pdf"}' \
https://formahand.com/api/product-files
GET /api/product-files?productId= lists them (products:read), DELETE /api/product-files?productId=&fileId= removes one and deletes the object (products:write). The MCP tools are list_product_files, upload_product_file (url, or dataBase64 + contentType up to 5 MB inline) and delete_product_file.
Removing a file stops every link already issued for it.
4. Download links
After payment, each digital line's files get a link on the store's own hostname:
https://<store host>/downloads/<token>
The token carries the order id, the file id and an expiry, signed with HMAC-SHA256 over download:<orderId>:<fileId>:<expiry> under a per-store secret that never leaves the store's database. Nothing per link is stored: a link is signed fresh every time a page or the API is read, so the buyer's thank-you page always shows working links.
- Links last 72 hours.
- The route streams the file from the store's bucket as an attachment,
Cache-Control: private, no-store, never indexed. - An expired link answers 410 with an explanation; a forged or unknown one answers 404. A file only opens for an order that is actually paid.
Where the links appear:
- The hosted thank-you page (
/checkout/success?session_id=…). - The order confirmation email, as
{{#each downloads}}over{{ this.name }}/{{ this.url }}. The default template renders that block only when the order has downloads, so nothing changes for a physical order.downloadsis part of the render context, so a custom template or a flow'ssend_emailcan use it too. - The public order lookup,
GET /api/storefront/orders?session_id=…, asdownloads: [{ name, url, expiresAt }]— that is how a custom front end shows them (see headless.md).
Re-issuing: Re-issue download links on the order in the dashboard, POST /api/product-files?orderId=<id> (needs orders:write for a token), or the regenerate_download_links { orderId } tool. Each call returns a fresh set with a new 72-hour window; the order and its files are untouched.
5. The storefront
- The product page of a digital or service product replaces shipping talk with one line — "Instant download — 2 files, no shipping." or "No shipping — booked and delivered by us." A backordered product shows its promise. An ordinary in-stock physical product shows nothing new, so existing shops look exactly as they did.
- The cart hides the destination fields when nothing in it ships.
- A backordered line stays in the cart at zero stock instead of being dropped.
6. The public read API
Product listings and the product detail carry three extra fields:
{ "slug": "field-notes", "kind": "digital",
"backorder": { "allowed": false },
"digital": { "files": 2 } }
digital.files is a count and nothing more — file names, sizes and keys are never public. kind is physical for every product a store has not changed, so a front end written before this page keeps working unchanged.
7. Limits
| Thing | Limit |
|---|---|
| Files per digital product | 10 |
| File size | 200 MB (5 MB when sent inline as base64 to a tool) |
| File types | pdf, zip, epub, mp3, mp4, png, jpg, txt, csv, json |
| Download link lifetime | 72 hours, re-issuable without limit |
| Backorder message | 160 characters |
Not yet: per-buyer download counts, licence keys, expiring access to a stream, and variant-level files (files belong to the product, and every variant of it delivers the same set).
8. The rest of the product
A kind says how a product is delivered; the fields in agents.md § Product details say what it is: vendor, productType, barcode (product and variant), costMinor (product and variant, never public), dimensions (product and variant), countryOfOrigin and hsCode for customs, and unitPrice for shops that sell by weight, volume or length. The product editor groups them the way a merchant looks for them — organisation on the right (status, vendor, type, tags, collections, and Customs folded away), cost and margin beside the price, barcode and "Sell when out of stock" in the Inventory row, package size beside the weight — and the Products section has a third tab, Inventory, with every variant's stock on one screen.
"Sell when out of stock" is this page's backorder.allowed under the name a merchant expects; there is one control for it, in the Inventory row, and the promise fields (ships by, what shoppers see) appear next to the kind once it is on.
See also
- agents.md — the file and download tools, their scopes, and where product kinds sit in the catalogue.
- custom-fields.md — licence terms, supplier ids and anything else a kind does not model.
- content-seo.md — per-page titles and descriptions for a digital product's page.
- shopper-accounts.md — a signed-in buyer finds their downloads again without an email.
- headless.md — how kinds, files and downloads appear to a front end you host yourself.