---
title: Resumable Uploads
slug: api-docs/resumable-uploads
description: How resumable, chunked uploads work in the Litmus API. Apply to firmware, AI/ML models, analytics packages, and large templates. Restart from byte offset on network failures.
docTags: 
createdAt: 2026-05-11T22:04:00.475Z
---

# Resumable Uploads

Litmus Edge uses a three-step resumable upload pattern for any operation that ingests a file: templates, backups, certificates, analytics models. The pattern is the same; only the path changes.

## The pattern

| Phase             | Method   | What it does                               | Body                |
| ----------------- | -------- | ------------------------------------------ | ------------------- |
| 0. Clear          | `DELETE` | Remove any incomplete/stale upload session | none                |
| 1. Create session | `POST`   | Declare file size, get a session `id` back | `{"size": <bytes>}` |
| 2. Upload         | `PUT`    | Stream the file bytes into the session     | binary file body    |

:::BlockQuote
**Why DELETE first?** If a previous upload was interrupted (network drop, browser close), the session persists on the server. Deleting it first ensures a clean state and prevents 409 Conflict.
:::

## Endpoints that use it

| Workflow                | Base path                        | Page            |
| ----------------------- | -------------------------------- | --------------- |
| Apply / Upload Template | `/dm/template/v2`                | [Workflow 1](#) |
| Restore Backup          | `/dm/backup/v2`                  | [Workflow 2](#) |
| Upload Custom CA        | `/dm/certstore` (no DELETE step) | [Workflow 3](#) |
| Upload Analytics Model  | `/analytics/v2/upload_model/v2`  | [Workflow 4](#) |

## Step 2 (PUT) URL shapes

The PUT URL is slightly different per module:

| Module          | PUT URL shape                                       |
| --------------- | --------------------------------------------------- |
| Template        | `/dm/template/v2/{session_id}/resume`               |
| Backup          | `/dm/backup/v2/{session_id}/resume`                 |
| Cert store      | `/dm/certstore/{session_id}` (no `/resume` suffix)  |
| Analytics model | `/analytics/v2/upload_model/v2/resume/{session_id}` |

## Example: apply a template

```bash
# Step 0: clear any stale session
curl -X DELETE "$EDGE/dm/template/v2" \
  -H "Authorization: Bearer $TOKEN"

# Step 1: declare size
SIZE=$(stat -c%s mytemplate.zip)
ID=$(curl -sX POST "$EDGE/dm/template/v2" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"size\": $SIZE}" | jq -r .id)

# Step 2: upload bytes
curl -X PUT "$EDGE/dm/template/v2/$ID/resume" \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @mytemplate.zip
```

## LEM uploads are different

LEM does **not** use this pattern. LEM ML model upload uses a pre-signed S3 URL:

1. `POST .../mlModel/{filename}/upload-url` returns a signed S3 URL
2. `PUT <signed url>` uploads bytes **directly to S3** (no auth header needed - SigV4 is baked into the URL)
3. `POST .../mlModel/{filename}` commits / registers the model in LEM

See [Upload AI/ML Model to LEM](#).
