Skip to content

Digital downloads ​

A digital product delivers one or more files after payment. This page covers how files and prices are attached, what the buyer sees, how the download link is authorized, and what is logged.

Creating a digital product ​

  1. Go to Ambikly → Products and click Add New Product.
  2. Enter a name and choose Digital product.
  3. Click Continue. The digital editor opens on a new draft.
  4. Upload files under Downloadable files.
  5. Add at least one plan under Pricing plans and give it a price.
  6. Write the description, then Publish.

This sets type = digital on the product row. is_downloadable defaults to 1 for digital products, and digital products never track stock — the editor sends manage_stock = 0, stock_quantity = null and stock_status = instock on every save.

Downloadable files ​

The Downloadable files card holds the product's shared file pool. Every pricing plan uses this pool unless it overrides it.

  1. Click Upload (or the dashed drop area when the pool is empty).
  2. Pick one or more files from the WordPress media library. Any file type works.
  3. Each file gets a version of 1.0.0. Edit it inline in the small v box.
  4. Remove a file with the × at the end of its row.

Files are stored in ambikly_product_files.

ColumnWhat it holds
product_idThe owning product.
pricing_plan_idNULL for a shared file; a plan ID for a plan-specific override.
nameDisplay name.
file_urlThe media library URL, run through esc_url_raw.
file_nameThe original file name.
versionFree-text version string, e.g. 2.3.1.
positionOrdering within its set.

On every save Ambikly replaces the whole set for that product and plan pair rather than diffing it. A file row with no url is skipped.

Tip: the file URL is stored as-is, so a file hosted on a CDN or an external origin works exactly like one in your uploads folder. Only files inside your own uploads directory can have their size recorded for download analytics.

Where files are served from ​

Ambikly stores the URL and redirects to it. It does not proxy the bytes and it does not move your uploads out of wp-content/uploads. A file in the media library keeps its normal, publicly guessable URL — the tokenized link controls the link you hand the customer, not access to the raw path. Put files you must protect behind a host that can enforce access on its own.

Pricing plans ​

A digital product prices through plans, not through the price column. Click Add plan (or Add first plan) to open the plan drawer.

FieldStored asNotes
Plan namenameRequired. Defaults to Standard for the first plan.
Priceprice, decimal(10,2)May not be negative.
Sale pricesale_priceOptional. Must be lower than the plan price.
Recurring billingis_subscriptionOff by default.
Bill everysubscription_cycleRecurring only. Default 1.
Periodsubscription_perioddaily, weekly, monthly or yearly.
Trial daystrial_periodRecurring only. Default 0.
Paymentssubscription_lengthRecurring only. Blank means no end.
Signup feesignup_feeRecurring only. May not be negative.
Downloadable filesambikly_product_files rowsShared by default; Override for this plan switches to a plan-specific list.

The drawer previews the billing line — for example $9.00 signup · $29.00 / monthly · 14-day trial — as soon as a price is set.

Plans are stored in ambikly_product_pricing_plans, ordered by position. Saving replaces the full set: a plan you removed from the form is deleted, along with its file overrides.

New plans carry a temporary client-side ID like plan-1718…. Only a numeric ID that already exists in the table is treated as an update target, so a new plan never overwrites an existing one.

Shared versus per-plan files ​

Each plan is in one of two states:

  • Uses shared files. The plan has no override rows. It delivers whatever is in the product's shared pool, including files you add later.
  • Overrides. The plan has its own list, even if that list is empty. The shared pool is ignored for this plan.

Switching a plan to Use shared files deletes its override rows on the next save.

Careful: when a product has license_enabled on, each plan drawer shows an "Activations per key" field. That value is sent on save but is not persisted — save_pricing_plans() does not write it and the API always reads it back as null. Per-plan activation limits do not work in the free core. The product-level license_activation_limit column is the value that is stored.

Download limits and expiry ​

Two product-level columns control how long and how often a granted file can be fetched.

ColumnMeaningEmpty means
download_limitMaximum number of successful downloads per grant.Unlimited
download_expiry_daysDays from the moment the grant is created until it stops working.Never expires
Careful: the digital product editor has no field for either value. The form carries them in its state and sends them on save, but no control is ever rendered, so they can only be set through the REST API (download_limit and download_expiry or download_expiry_days on POST/PUT /products) or through the CSV importer (download_limit and download_expiry_days columns). Saving the product from the admin editor does not clear values set elsewhere — the form sends null and the update path skips null keys.

Both values are copied onto each grant at the moment the grant is created. Changing them on the product afterwards does not alter grants that already exist.

What happens when an order is paid ​

On the ambikly_order_paid event, Ambikly walks the order's items and, for each item that is downloadable or whose product type is digital:

  1. Loads the product's files. When the line item carries a pricing_plan_id, it takes the shared files plus that plan's overrides.
  2. Skips the item entirely if no files are attached — no phantom grant is created.
  3. Reads download_limit and download_expiry_days from the product and turns the expiry into an absolute timestamp.
  4. Inserts one row in ambikly_downloads per file, each with its own opaque token.

The step is idempotent: a grant for an existing order, order item and file triple is left alone, so a replayed payment webhook does not double-issue.

An order note records how many grants were issued.

Tokens are 48 hexadecimal characters from random_bytes(24). On a host without random_bytes the fallback is an MD5 of a unique ID plus a generated password.

Digital-only orders auto-complete ​

If every item on the order is digital or downloadable, Ambikly immediately moves the order to Completed and logs Digital-only order auto-completed. A mixed cart stays in Processing so you still fulfill the physical part. See Orders.

License keys ​

Ticking Generate license keys on a digital product sets license_enabled. Keys are minted on the same ambikly_order_paid event and an order note records how many were issued. A full refund revokes them and logs that too. See Refunds & cancellations.

The download endpoint ​

GET /wp-json/ambikly/v1/downloads/{token}

The route is public. The token pattern is [a-f0-9]{32,64}. There is no login check and no nonce — knowing the token is the permission. That is deliberate: the link is emailed to the buyer and shown in their account, and it has to work from a mail client.

Every request runs these checks in order, and any failure returns HTTP 403 with code download_denied and a plain message.

CheckMessage on failure
The token matches a grantInvalid download token.
The grant has not passed access_expires_atDownload link has expired.
download_count is below download_limitDownload limit reached.
The order still existsOrder no longer exists.
The order is not cancelled or refundedThis order has been cancelled.
Payment is paid or partially-paid, or the order status is processing or completedPayment is not yet confirmed for this order.

Cancelling a paid order is enough to kill its download links on its own. A plain cancellation leaves payment_status at paid, so the status check is what stops access.

On success, Ambikly:

  1. Increments download_count.
  2. Sets last_downloaded_at.
  3. Appends {ip, at} to the grant's IP log.
  4. Fires the ambikly_download_served action.
  5. Issues a 302 redirect to the file URL.

The URL passes through the ambikly_download_file_url filter before the redirect, so an add-on can swap in a signed or stamped copy.

If the grant somehow has no valid URL the endpoint returns 500 with no_file_url.

IP logging ​

Each grant carries an ip_log column holding a JSON array of {"ip": "…", "at": "…"} entries, one per successful download, trimmed to the last 20. The IP is taken from the first of HTTP_CF_CONNECTING_IP, HTTP_X_FORWARDED_FOR or REMOTE_ADDR that is set, splitting on the first comma.

There is no admin screen that renders this log in the free core. Read it directly from ambikly_downloads, or use the Download Analytics add-on, which listens on ambikly_download_served.

What the buyer sees ​

The product page ​

A digital product page shows a Choose a plan radio group when the product has pricing plans, with each plan's name, effective price and billing period. The product's own price is deliberately not printed above the plans, because it stays at 0 for a plan-priced product.

The buy button reads Buy now rather than Add to Cart, and the form carries an immediate-checkout flag so the customer goes straight to checkout instead of the cart page. The quantity field is hidden and fixed at 1.

The Downloads tab ​

After payment, files appear under My account → Downloads on your storefront account page — the ?ambikly_tab=downloads view of the account page.

ColumnContents
FileThe grant's file name, or "Download" when it has none.
Usescount when unlimited, count / limit when a limit is set.
ExpiresThe expiry timestamp, or "No expiry".
—A Download button pointing at the tokenized URL.

The table shows 50 rows per page and pages with ?ap=. One row appears per order item and file pair, so a plan with four files produces four rows for one purchase.

Before any digital order is paid, the tab reads: No downloadable files yet. Once a digital order is paid your files will appear here.

Reading grants through the API ​

MethodRoutePermission
GET/ambikly/v1/downloads/{token}Public — the token is the credential
GET/ambikly/v1/orders/{id}/downloadsView store
GET/ambikly/v1/customers/{id}/downloadsView store
GET/ambikly/v1/account/downloadsAny logged-in user, scoped to their own customer record

The three list routes return token, file_name, count, limit, expires_at and a ready-made url. The customer and account routes also include order_id and product_id.

/account/downloads returns an empty list, not an error, when the logged-in user has no customer record yet.

Limits worth knowing ​

  • Grants are created from files. A digital product with no files attached produces no grants and the customer's Downloads tab stays empty, even though the order completed.
  • A file's version is stored and displayed in the admin, but the download endpoint always serves the current file_url. There is no version history and no "download an older build" flow in the free core.
  • helpful_count, restrictDownloads, allowedRoles and hideDownloadLinks appear in the product form's internal state but have no control and no server-side reader. Role-restricted downloads are not implemented.
  • There is no bulk "regenerate all download links" action.

Pro add-ons ​

PROMore from your digital catalog

Stamp buyer details into delivered PDFs, report on who downloaded what, and turn license keys into a full activation API with remote checks and site limits.

See pricing →
  • PDF Stamping — watermark each delivered PDF with the buyer's details.
  • Download Analytics — per-product and per-customer download reporting, driven by ambikly_download_served.
  • License Pro — activation limits, remote validation and a license management screen.

Troubleshooting ​

SymptomCause
Order completed but the Downloads tab is emptyNo files are attached to the product, or the plan overrides files with an empty list.
"Payment is not yet confirmed for this order."The order is not paid or partially-paid, and its status is not processing or completed.
"This order has been cancelled."The order was cancelled or refunded. Cancelling alone is enough — the payment status is not checked.
"Download limit reached."download_count has hit the grant's download_limit. Limits are fixed on the grant at creation; changing the product does not raise them.
A download limit set through the API vanishedIt did not. The admin editor has no field for it, so it is invisible there — read it back with GET /products/{id}.
The order stayed in ProcessingAt least one item on the order is physical. Only fully digital orders auto-complete.

Next ​