Digital products
Traide supports digital goods as first-class catalog citizens on the same multivendor rails as physical goods. Digital lines flow through per-seller suborders, commissions, payouts, and the double-entry financial engine exactly like physical lines — there is no separate "digital checkout" or side-channel. What changes is fulfillment: for the file-based goods this page covers, a paid line produces a secure, expiring download link, an email to the buyer, and a durable entitlement recording what the customer owns.
This page covers modelling, upload, delivery, and reading delivery state for file-based digital goods. The companion pages: Entitlements & refunds covers the ownership ledger, the customer library, refund-driven revocation, and webhooks; License keys, codes & gift cards covers the per-unit credential kinds — pools, reveal, the verification API, and the refund flip. The operator-facing walkthrough lives in the user guide: Digital products.
How digital products are modelled
Every product template, product, and variant carries a fulfillment kind:
enum FulfillmentKind {
SHIPMENT
DIGITAL_FILE
SERVICE
LICENSE_KEY
CODE
GIFT_CARD
}
SHIPMENT— physical goods (the default).DIGITAL_FILE— delivered as a downloadable file. This kind drives everything on this page.SERVICE— non-shippable service lines. No delivery automation runs for this kind today.LICENSE_KEY/CODE/GIFT_CARD— per-unit secret credentials: every unit sold is a distinct key with its own lifecycle and entitlement, delivered by reveal rather than download. Covered in License keys, codes & gift cards; marketplaces enable them explicitly viaenabledFulfillmentKinds.
The kind waterfalls: setting it on a product template provides the default for products created under it, and a product's kind provides the default for its variants. Because the kind lives at the variant level, one product can sell a digital and a physical variant side by side (an ebook and a paperback under the same product).
The legacy booleans isDigital and isShippingRequired are still populated for compatibility, derived from the kind. When branching on product behavior, read fulfillmentKind, not the booleans — the kind is the typed source of truth, and on order lines it is snapshotted at purchase time while the booleans on catalog objects can change afterwards.
Which kinds a marketplace accepts is controlled by MarketplaceConfiguration.enabledFulfillmentKinds. If a kind is not enabled for the marketplace, catalog writes using it are rejected.
Create a digital product
Set fulfillmentKind: DIGITAL_FILE when creating the product (or template, or variant — whichever level you want the setting to originate from):
mutation {
productCreate(
input: {
name: "Marketing Playbook (PDF)"
description: "A 120-page marketing playbook, delivered as a PDF download."
productType: "UHJvZHVjdFR5cGU6MTg="
seller: "U2VsbGVyOjI="
basePrice: 49.99
fulfillmentKind: DIGITAL_FILE
}
) {
product {
id
name
fulfillmentKind
isDigital
isShippingRequired
}
productErrors {
field
code
message
}
}
}
Variants inherit the product's kind, and productVariantCreate / productVariantUpdate accept their own fulfillmentKind for mixed products.
Upload the file
Each digital variant carries one digital content record — the file plus its delivery settings. Create it with digitalContentCreate, which takes the variant ID and a DigitalContentUploadInput:
| Input field | Type | Meaning |
|---|---|---|
contentFile | Upload! | The file itself |
useDefaultSettings | Boolean! | true = inherit the marketplace's digital defaults; false = use the overrides below |
automaticFulfillment | Boolean | Fulfill this variant's lines automatically when the order is fully paid |
maxDownloads | Int | Downloads allowed per issued link (empty = no cap) |
urlValidDays | Int | Days an issued link stays valid (empty = no expiry) |
File upload uses the standard GraphQL multipart request form, with the usual authentication headers (see Authentication):
curl https://<your-api-host>/graphql/ \
-H "Authorization: JWT <token>" \
-F operations='{"query":"mutation($variantId: ID!, $input: DigitalContentUploadInput!){ digitalContentCreate(variantId: $variantId, input: $input){ content { id automaticFulfillment maxDownloads urlValidDays } productErrors { field code message } } }","variables":{"variantId":"UHJvZHVjdFZhcmlhbnQ6MTIz","input":{"useDefaultSettings":true,"contentFile":null}}}' \
-F map='{"0":["variables.input.contentFile"]}' \
-F 0=@playbook.pdf
Manage the record afterwards with:
digitalContentUpdate(variantId, input: DigitalContentInput!)— change settings, or replace the file viacontentFile.digitalContentDelete(variantId)— remove the file and settings from the variant.digitalContent(id)/digitalContents(first: …)— query digital content records; each exposes its settings and its issuedurls.DigitalContent.auditEvents— a database-level audit trail of who changed what, when. Reading it requiresMANAGE_PRODUCTS.
Marketplace-wide defaults
Operators set delivery defaults once, on the marketplace configuration; any content record with useDefaultSettings: true inherits them:
mutation {
marketplaceConfigurationUpdate(
input: {
automaticFulfillmentDigitalProducts: true
defaultDigitalMaxDownloads: 5
defaultDigitalUrlValidDays: 30
}
) {
marketplaceConfiguration {
automaticFulfillmentDigitalProducts
defaultDigitalMaxDownloads
defaultDigitalUrlValidDays
}
}
}
The same section of the configuration also carries revokeEntitlementsOnRefund, covered in Entitlements & refunds.
The delivery flow
- Checkout — a checkout containing only digital lines does not require a shipping method. Mixed carts keep their normal per-seller shipping flow for the physical lines.
- Payment — when the order becomes fully paid and automatic fulfillment applies (from the content record's own setting or the marketplace default), every digital line is fulfilled automatically: a
DigitalContentUrlis minted per line, the buyer receives the digital-links email, and an entitlement is granted. - Manual fulfillment — if automatic fulfillment is off, fulfilling a digital line through the normal fulfillment mutations mints the link and sends the email at that point instead.
- Download — the buyer's link streams the file as an attachment. Each download increments the link's counter, and the link stops resolving once
maxDownloadsis exhausted orurlValidDayshas elapsed. Every download is also recorded server-side — as a customer analytics event (DIGITAL_LINK_DOWNLOADED) and as a redemption event with request forensics, which is the record a chargeback dispute is argued from.
Read delivery state on orders
Both order tiers expose the same two line-level fields — the snapshotted kind and the issued link:
query {
nauticalOrder(id: "TmF1dGljYWxPcmRlcjox") {
lines {
productName
fulfillmentKind
digitalContentUrl {
url
created
downloadNum
content {
maxDownloads
urlValidDays
}
}
}
}
}
fulfillmentKind on a line is a purchase-time snapshot — it tells you what was actually sold, even if the variant changed later. Branch on DIGITAL_FILE to decide whether to render delivery state. digitalContentUrl is null until a link is issued (payment + automatic fulfillment, or manual fulfillment). Seller-tier OrderLine carries the identical fields, and both are DataLoader-backed, so requesting them across a page of orders stays N+1-safe.
Re-issue or add links
Two mutations manage issued links:
digitalContentUrlRotate(lineId)— re-issue the link for an order line. The existing link stops working immediately and is replaced by one with a fresh token and a reset download count. It accepts either anOrderLineor aNauticalOrderLineID, leaves the customer's entitlement untouched (it replaces the delivery credential, not ownership), and requiresMANAGE_ORDERS. Use it when a link leaked or a legitimate buyer exhausted their downloads.digitalContentUrlCreate(input: { content })— mint an additional link directly against a digital content record.
Seller digital gating
Whether a given seller may sell digital goods is a per-seller switch:
mutation {
sellerDataUpdate(
id: "U2VsbGVyOjI="
input: { digitalSalesStatus: SUSPENDED }
) {
seller {
id
digitalSalesStatus
}
sellerErrors {
field
code
message
}
}
}
- Sellers default to
APPROVED— digital selling is open unless the operator says otherwise. - Setting
SUSPENDEDimmediately unpublishes the seller's digital-kind products (their physical catalog is untouched), and those products report aSELLER_DIGITAL_SUSPENDEDwarning for as long as the suspension lasts. - Setting the seller back to
APPROVEDclears the warning everywhere at once, but does not republish — republishing the affected products is a deliberate, separate step.
Shopify ingestion
Products ingested through the Shopify connector map requiresShipping: false to the digital kind automatically, so a seller's digital catalog round-trips from Shopify without manual re-typing.