Skip to content

Upload Asset

Upload a binary file directly to Filemonk storage and turn it into an internal asset.

Last updated

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”
Terminal window
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"
}
}

PUT the file directly to upload.url. The URL is valid for one hour.

Terminal window
curl -X PUT "$URL" \
-H "Content-Type: application/zip" \
-H "Content-Disposition: inline; filename=\"brushes-pack-v2.zip\"" \
--upload-file ./brushes-pack-v2.zip
const 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.

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 order
await 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 customer
await fetch(`${BASE}/orders/${orderId}/notify`, {
method: 'POST',
headers: {
'X-Auth-Token': API_KEY,
'Content-Type': 'application/json'
}
});
Terminal window
# Attach the uploaded asset to an order
curl -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 email
curl -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 order
requests.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 email
requests.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.

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.

Terminal window
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 key of the underlying R2 object is namespaced to your shop (external_api/<shop_id>/...) — you do not need to manage keys yourself.
  • watermark_asset and autogenerate_licence_key work the same way for uploaded assets as for external / licence types — pass them on the create call if needed.
Was this page helpful?

Still stuck? Ask Quinn or chat with support