Upload Asset
Upload a binary file directly to Filemonk storage and turn it into an internal asset.
Last updated
Overview
Section titled “Overview”Filemonk’s direct upload API is built around a single POST /assets call. For
internal assets the response includes the presigned URL you use to upload the
file body — there is no separate “initiate” step.
The per-file ceiling is 2 GB. Need to upload larger files via the API? Get in touch.
All endpoints use the same authentication (X-Auth-Token) and rate-limit
bucket as the rest of the API. Plan storage and asset-count quotas are
enforced before any upload URL is issued — calls that would exceed your quota
return 422 with code: asset_storage_limit_exceeded or
asset_count_limit_exceeded.
1. Create the asset and get the upload URL
Section titled “1. Create the asset and get the upload URL”curl -X POST 'https://app.filemonk.io/api/v1/external/assets' \ -H 'X-Auth-Token: fm_api_key_your_key_here' \ -H 'Content-Type: application/json' \ -d '{ "asset": { "type": "internal", "filename": "brushes-pack-v2.zip", "byte_size": 1048576, "content_type": "application/zip" } }'const stats = await stat('./brushes-pack-v2.zip');
const create = await fetch( 'https://app.filemonk.io/api/v1/external/assets', { method: 'POST', headers: { 'X-Auth-Token': 'fm_api_key_your_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ asset: { type: 'internal', filename: 'brushes-pack-v2.zip', byte_size: stats.size, content_type: 'application/zip' } }) }).then(r => r.json());
const { asset, upload } = create;byte_size = os.path.getsize('./brushes-pack-v2.zip')
create = requests.post( 'https://app.filemonk.io/api/v1/external/assets', headers={'X-Auth-Token': 'fm_api_key_your_key_here'}, json={ 'asset': { 'type': 'internal', 'filename': 'brushes-pack-v2.zip', 'byte_size': byte_size, 'content_type': 'application/zip' } }).json()
asset = create['asset']upload = create['upload']Response:
{ "asset": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "internal", "filename": "brushes-pack-v2.zip", "content_type": "application/zip", "byte_size": 1048576, "...": "..." }, "upload": { "method": "PUT", "url": "https://<bucket>.r2.cloudflarestorage.com/external_api/<shop_id>/8f3a.zip?X-Amz-Signature=...", "headers": { "Content-Type": "application/zip", "Content-Disposition": "inline; filename=\"brushes-pack-v2.zip\"" }, "expires_at": "2026-04-08T11:00:00Z" }}2. Upload the file body
Section titled “2. Upload the file body”PUT the file directly to upload.url. The URL is valid for one hour.
curl -X PUT "$URL" \ -H "Content-Type: application/zip" \ -H "Content-Disposition: inline; filename=\"brushes-pack-v2.zip\"" \ --upload-file ./brushes-pack-v2.zipconst body = await readFile('./brushes-pack-v2.zip');
const put = await fetch(upload.url, { method: 'PUT', headers: upload.headers, body});
if (!put.ok) { throw new Error(`Upload failed: ${put.status} ${await put.text()}`);}with open('./brushes-pack-v2.zip', 'rb') as f: put = requests.put(upload['url'], headers=upload['headers'], data=f)
put.raise_for_status()That’s it — the asset is ready to be linked to a Shopify product via
POST /digital_products,
or attached directly to an order via
POST /orders/{id}/attach_assets.
Putting it together: multiple files on one product
Section titled “Putting it together: multiple files on one product”A common pattern is uploading several files for the same Shopify product
(e.g. high-res + low-res versions, or chapter-by-chapter PDFs). Create each
asset, upload its body, then attach all of them in a single
POST /digital_products call:
const API_KEY = 'fm_api_key_your_key_here';const BASE = 'https://app.filemonk.io/api/v1/external';
async function uploadFile(path, filename, contentType) { const { size } = await stat(path);
const { asset, upload } = await fetch(`${BASE}/assets`, { method: 'POST', headers: { 'X-Auth-Token': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ asset: { type: 'internal', filename, byte_size: size, content_type: contentType } }) }).then(r => r.json());
const body = await readFile(path); const put = await fetch(upload.url, { method: 'PUT', headers: upload.headers, body }); if (!put.ok) { // Free quota on failure so we don't leave an orphan. await fetch(`${BASE}/assets/${asset.id}`, { method: 'DELETE', headers: { 'X-Auth-Token': API_KEY } }); throw new Error(`Upload failed for ${filename}: ${put.status}`); }
return asset.id;}
const files = [ ['./brushes-hi-res.zip', 'brushes-hi-res.zip', 'application/zip'], ['./brushes-low-res.zip', 'brushes-low-res.zip', 'application/zip'], ['./README.pdf', 'README.pdf', 'application/pdf']];
const assetIds = [];for (const [path, name, ct] of files) { assetIds.push(await uploadFile(path, name, ct));}
await fetch(`${BASE}/digital_products`, { method: 'POST', headers: { 'X-Auth-Token': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ digital_product: { shopify_product_id: 8847261098265, asset_ids: assetIds, custom_message: 'Thanks for your purchase!' } })});To add files to a product that already exists, fetch the current asset_ids
from GET /digital_products/{id}, union them with the new ones, and PATCH
with the full list — the update is a replace, not an append.
Delivering a file to a specific order
Section titled “Delivering a file to a specific order”For custom or made-to-order products, you often need to upload a file and deliver it to a single order rather than linking it to a Shopify product. After uploading, attach the asset to an order and trigger the customer notification email:
// Steps 1 & 2 from above — create the asset and PUT the file body.// Then attach to an order and notify the customer:
const orderId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
// Attach asset to the orderawait fetch(`${BASE}/orders/${orderId}/attach_assets`, { method: 'POST', headers: { 'X-Auth-Token': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ asset_ids: [asset.id] })});
// Send the download email to the customerawait fetch(`${BASE}/orders/${orderId}/notify`, { method: 'POST', headers: { 'X-Auth-Token': API_KEY, 'Content-Type': 'application/json' }});# Attach the uploaded asset to an ordercurl -X POST "https://app.filemonk.io/api/v1/external/orders/$ORDER_ID/attach_assets" \ -H 'X-Auth-Token: fm_api_key_your_key_here' \ -H 'Content-Type: application/json' \ -d "{\"asset_ids\": [\"$ASSET_ID\"]}"
# Trigger the customer notification emailcurl -X POST "https://app.filemonk.io/api/v1/external/orders/$ORDER_ID/notify" \ -H 'X-Auth-Token: fm_api_key_your_key_here' \ -H 'Content-Type: application/json'# Attach the uploaded asset to an orderrequests.post( f'{BASE}/orders/{order_id}/attach_assets', headers={'X-Auth-Token': API_KEY}, json={'asset_ids': [asset_id]}).raise_for_status()
# Trigger the customer notification emailrequests.post( f'{BASE}/orders/{order_id}/notify', headers={'X-Auth-Token': API_KEY}).raise_for_status()The customer will receive a download email containing the newly attached file.
You can also use an external asset (pass external_url instead of uploading)
— the same attach + notify flow works for any asset type.
Cleaning up abandoned uploads
Section titled “Cleaning up abandoned uploads”If you create an asset but never PUT the file body (or the PUT fails and
you don’t retry), the asset row still counts against your plan’s storage and
asset-count quotas. Call DELETE /assets/{id} to free quota immediately —
it removes the asset row, the underlying blob, and the R2 object in a single
call.
curl -X DELETE 'https://app.filemonk.io/api/v1/external/assets/<asset_id>' \ -H 'X-Auth-Token: fm_api_key_your_key_here'- Plan quota is checked at create time using the declared
byte_size. - Uploads must finish within one hour of the URL being issued.
- The
keyof the underlying R2 object is namespaced to your shop (external_api/<shop_id>/...) — you do not need to manage keys yourself. watermark_assetandautogenerate_licence_keywork the same way for uploaded assets as forexternal/licencetypes — pass them on the create call if needed.
Thanks for letting us know.
Still stuck? Ask Quinn or chat with support