> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mediamagicverify.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Submit assets guide

> Create submissions and manage multiple assets

## Preview the cost before submitting

Creating a submission consumes credits. To see the cost first, call `POST /api/pricing/estimate` with the same `blobPath`s you intend to submit — nothing else is needed. Do this **after uploading your assets** and **before** creating the submission — it is read-only and consumes nothing.

The asset type and MIME type are derived from the blobPath server-side (the same way the submission does), and the price is computed from a metric measured on the uploaded bytes (video duration, document page count) — so the estimate matches what you are charged when you submit. The measurement is cached, so the submission that follows does not re-read the file.

<CodeGroup>
  ```python theme={null}
  import requests

  API_KEY = "your-api-key-here"
  API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"

  # blobPaths returned by your earlier uploads
  estimate = requests.post(
      f"{API_BASE}/api/pricing/estimate",
      headers={"X-API-Key": API_KEY},
      json={
          "files": [
              {"blobPath": "<asset-id>/campaign-video.mp4"},
              {"blobPath": "<asset-id>/brief.pdf"},
          ]
      },
  ).json()

  print(f"This submission will cost {estimate['totalCredits']} credits")
  for f in estimate["files"]:
      print(f"  {f['blobPath']}: {f['credits']} credits (priceable={f['priceable']})")
  ```

  ```bash curl theme={null}
  curl -X POST https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/pricing/estimate \
    -H "X-API-Key: your-api-key-here" \
    -H "Content-Type: application/json" \
    -d '{
      "files": [
        { "blobPath": "<asset-id>/campaign-video.mp4" },
        { "blobPath": "<asset-id>/brief.pdf" }
      ]
    }'
  ```
</CodeGroup>

Each result echoes its `blobPath` so you can map it back to the file you sent. The response returns the cost per file plus a `totalCredits` for the batch:

```json theme={null}
{
  "files": [
    { "blobPath": "<asset-id>/campaign-video.mp4", "assetType": "video",
      "metric": "durationSeconds", "metricValue": 42, "credits": 3, "priceable": true },
    { "blobPath": "<asset-id>/brief.pdf", "assetType": "document",
      "metric": "pages", "metricValue": 8, "credits": 1, "priceable": true }
  ],
  "totalCredits": 4
}
```

<Note>
  A file with `priceable: false` could not be measured (for example an unreadable PDF). It costs `0` here and is excluded from `totalCredits`, but it will be **rejected** when you create the submission — re-upload it as a readable file first.
</Note>

## Creating a submission

A submission is a request to process one or more uploaded assets. After creating a submission, the API queues your assets for processing.

Each asset object carries `blobPath` (the path returned by an upload) and, optionally, `previousSubmissionId` for versioning and a per-asset `sidekickIds` list (see [Choosing sidekicks](#choosing-sidekicks) below). Do not send `filename` or `contentType` in asset objects — they are not part of the schema. The other top-level fields are `sidekickIds` (**required** — see below), `name` (an optional display name for the submission, up to 120 characters — defaults to the first asset's filename if omitted), and `metadata` (an optional arbitrary object).

### Choosing sidekicks

`sidekickIds` is the submission-level list of sidekicks to run for compliance checking. **At least one is required** — a submission with no sidekick has nothing to check, so a request that omits `sidekickIds` or sends an empty list is rejected with `422`.

To discover which sidekicks your workspace can run — and the asset types each one accepts — call [List available sidekicks](/sidekicks/list-sidekicks) and use the returned `id` values. An asset whose type no selected sidekick supports (e.g. a PDF paired only with a video sidekick) is rejected at submit time, so make sure each asset is covered by a sidekick that handles its type.

**Per-asset selection (optional).** Each asset may also carry its own `sidekickIds` to run only a subset of the sidekicks on that asset. A per-asset list must be a subset of the submission-level `sidekickIds`; an empty (or omitted) per-asset list means the asset inherits the full submission-level set. Use this when different assets in one submission need different checks — keep the submission-level list as the union of every asset's selection.

<Warning>
  **Breaking change (June 2026).** `sidekickIds` was previously optional and accepted an empty list. It is now required with at least one entry. Clients that currently send `"sidekickIds": []` must be updated to pass one or more sidekick IDs (fetch them from [List available sidekicks](/sidekicks/list-sidekicks)) before upgrading.
</Warning>

<Tip>
  Testing with a sandbox (`sk_test_`) key? The asset filename must contain a scenario marker so the sandbox knows which outcome to simulate. See the [Sandbox environment](/reference/sandbox#choosing-a-test-scenario) guide.
</Tip>

### Single asset submission

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/submissions \
    -H "X-API-Key: your-api-key-here" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Spring product launch",
      "assets": [
        { "blobPath": "<workspace-id>/recording.mp4" }
      ],
      "sidekickIds": ["<sidekick-id>"],
      "metadata": { "campaign": "spring-2026" }
    }'
  ```

  ```python theme={null}
  import requests

  API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"

  response = requests.post(
      f"{API_BASE}/api/submissions",
      headers={"X-API-Key": "your-api-key-here"},
      json={
          "name": "Spring product launch",
          "assets": [
              {"blobPath": "<workspace-id>/recording.mp4"}
          ],
          "sidekickIds": ["<sidekick-id>"],
          "metadata": {"campaign": "spring-2026"},
      },
  )

  submission = response.json()
  print(f"Submission ID: {submission['submissionId']}")
  ```

  ```javascript theme={null}
  const API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations";

  const response = await fetch(
    `${API_BASE}/api/submissions`,
    {
      method: "POST",
      headers: {
        "X-API-Key": "your-api-key-here",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "Spring product launch",
        assets: [{ blobPath: "<workspace-id>/recording.mp4" }],
        sidekickIds: ["<sidekick-id>"],
        metadata: { campaign: "spring-2026" },
      }),
    }
  );

  const submission = await response.json();
  console.log("Submission ID:", submission.submissionId);
  ```
</CodeGroup>

### Multiple asset submission

Submit multiple assets in a single request for batch processing:

```json theme={null}
{
  "assets": [
    { "blobPath": "<workspace-id>/video1.mp4" },
    { "blobPath": "<workspace-id>/video2.mp4" },
    { "blobPath": "<workspace-id>/contract.pdf", "sidekickIds": ["<document-sidekick-id>"] }
  ],
  "sidekickIds": ["<video-sidekick-id>", "<document-sidekick-id>"],
  "metadata": { "campaign": "spring-2026" }
}
```

The two videos inherit the full submission-level set, while `contract.pdf` narrows its checks to just the document sidekick via its own `sidekickIds`. The per-asset list stays a subset of the submission-level `sidekickIds`.

<Tip>
  Each asset is processed independently. Batch related assets into one submission to reduce request volume.
</Tip>

## Attaching tags and supporting context

A submission can also carry top-level `tags` and `contextItems` (both optional). Tags are key/value labels (found-or-created automatically — no pre-creation or UUID lookup needed). Context items are supporting **files** or **links** the review can use. Each entry's `applyTo` is `"all"` (default) or a list of asset **blobPaths**.

A context **file** is uploaded the same way as an asset — request an upload URL, `PUT` the bytes, then reference the returned `blobPath`:

<CodeGroup>
  ```python theme={null}
  import requests

  API_KEY = "your-api-key-here"
  API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"

  def upload(filename: str, content_type: str, path: str) -> str:
      data = requests.post(
          f"{API_BASE}/api/uploads/url",
          headers={"X-API-Key": API_KEY},
          json={"filename": filename, "contentType": content_type},
      ).json()
      with open(path, "rb") as f:
          requests.put(
              data["uploadUrl"], headers={"x-ms-blob-type": "BlockBlob"}, data=f
          ).raise_for_status()
      return data["blobPath"]

  asset_blob = upload("recording.mp4", "video/mp4", "recording.mp4")
  # Context files allow document types (PDF, DOCX, TXT) as well as media.
  evidence_blob = upload("evidence.pdf", "application/pdf", "evidence.pdf")

  submission = requests.post(
      f"{API_BASE}/api/submissions",
      headers={"X-API-Key": API_KEY},
      json={
          "assets": [{"blobPath": asset_blob}],
          "tags": [{"key": "Campaign", "value": "Summer", "applyTo": "all"}],
          "contextItems": [
              {
                  "kind": "file",
                  "contextType": "substantiation",
                  "blobPath": evidence_blob,
                  "name": "evidence.pdf",
                  "mimeType": "application/pdf",
                  "applyTo": "all",
              },
              {
                  "kind": "link",
                  "contextType": "legal",
                  "url": "https://example.com/brand-guidelines",
                  "linkName": "Brand guidelines",
                  "applyTo": "all",
              },
          ],
      },
  ).json()
  print(submission["submissionId"])
  ```

  ```bash curl theme={null}
  curl -X POST https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/submissions \
    -H "X-API-Key: your-api-key-here" \
    -H "Content-Type: application/json" \
    -d '{
      "assets": [{ "blobPath": "<workspace-id>/recording.mp4" }],
      "tags": [{ "key": "Campaign", "value": "Summer", "applyTo": "all" }],
      "contextItems": [
        { "kind": "file", "contextType": "substantiation",
          "blobPath": "<workspace-id>/evidence.pdf", "name": "evidence.pdf",
          "mimeType": "application/pdf", "applyTo": "all" },
        { "kind": "link", "contextType": "legal",
          "url": "https://example.com/brand-guidelines",
          "linkName": "Brand guidelines", "applyTo": "all" }
      ]
    }'
  ```
</CodeGroup>

See [Tags and supporting context](/concepts/submissions#tags-and-supporting-context) for the full field reference and the allowed `contextType` values.

## Response format

A successful create returns `201` with this shape:

```json theme={null}
{
  "submissionId": "550e8400-e29b-41d4-a716-446655440000",
  "workflowId": "asset-processing-...",
  "ablyChannel": "submission:550e8400-...",
  "status": "submitted",
  "assets": [
    {
      "assetId": "a1b2...",
      "versionGroupId": "g1...",
      "versionNumber": 1
    }
  ]
}
```

Read the identifier from `submissionId` (not `id`). You'll use it to check status and retrieve results.

## Asset versioning

Versioning is explicit. To create a new version of an asset, submit a new submission whose asset references the prior asset through `previousSubmissionId`. The new submission becomes the next version in the same version group.

<Steps>
  <Step title="Upload the revised file">
    Upload the corrected file and get its `blobPath`.
  </Step>

  <Step title="Reference the previous submission">
    Include `previousSubmissionId` on the asset so the API links it as the next version.
  </Step>

  <Step title="List versions">
    Use `/submissions/{submission_id}/versions` to list all versions in the group.
  </Step>
</Steps>

Example: resubmitting after fixing audio issues.

```python theme={null}
import requests

API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"

# The original submission whose asset you want to supersede
previous_submission_id = "550e8400-e29b-41d4-a716-446655440000"

response = requests.post(
    f"{API_BASE}/api/submissions",
    headers={"X-API-Key": "your-api-key-here"},
    json={
        "assets": [
            {
                "blobPath": "<workspace-id>/recording-fixed.mp4",
                "previousSubmissionId": previous_submission_id,
            }
        ],
        "sidekickIds": ["<sidekick-id>"],
    },
)

new_submission = response.json()
print(f"New version submission: {new_submission['submissionId']}")
print(f"Version number: {new_submission['assets'][0]['versionNumber']}")
```

List all versions in the group:

```bash theme={null}
curl -H "X-API-Key: your-api-key-here" \
  https://api.northell.io/nhl/prod/mediamagic-verify-integrations/api/submissions/550e8400-e29b-41d4-a716-446655440000/versions
```

Response:

```json theme={null}
{
  "versions": [
    {
      "assetId": "a1...",
      "submissionId": "550e8400-...",
      "versionNumber": 1,
      "name": "recording.mp4",
      "createdAt": "2026-06-03T12:00:00Z",
      "status": "complete",
      "issueCount": 3
    },
    {
      "assetId": "a2...",
      "submissionId": "660e8400-...",
      "versionNumber": 2,
      "name": "recording-fixed.mp4",
      "createdAt": "2026-06-04T10:00:00Z",
      "status": "complete",
      "issueCount": 1
    }
  ],
  "total": 2
}
```

## Batch submission example

A complete example of uploading multiple files and creating a batch submission:

<CodeGroup>
  ```python theme={null}
  import requests
  from pathlib import Path

  API_KEY = "your-api-key-here"
  API_BASE = "https://api.northell.io/nhl/prod/mediamagic-verify-integrations"

  def batch_submit_files(file_paths: list[str]) -> str:
      """Upload files via presigned URLs and create one batch submission."""

      assets = []
      for file_path in file_paths:
          file = Path(file_path)

          # Get a presigned URL
          url_response = requests.post(
              f"{API_BASE}/api/uploads/url",
              headers={"X-API-Key": API_KEY},
              json={
                  "filename": file.name,
                  "contentType": get_content_type(file.suffix),
              },
          )
          url_response.raise_for_status()
          data = url_response.json()

          # Upload the file to Azure
          with open(file_path, "rb") as f:
              requests.put(
                  data["uploadUrl"],
                  headers={"x-ms-blob-type": "BlockBlob"},
                  data=f,
              ).raise_for_status()

          assets.append({"blobPath": data["blobPath"]})
          print(f"Uploaded {file.name}")

      # Create one submission with all assets
      response = requests.post(
          f"{API_BASE}/api/submissions",
          headers={"X-API-Key": API_KEY},
          json={"assets": assets, "sidekickIds": ["<sidekick-id>"]},
      )
      response.raise_for_status()

      submission = response.json()
      print(f"Submission created: {submission['submissionId']}")
      return submission["submissionId"]

  def get_content_type(suffix: str) -> str:
      types = {
          ".mp4": "video/mp4",
          ".mp3": "audio/mpeg",
          ".pdf": "application/pdf",
          ".txt": "text/plain",
      }
      return types.get(suffix.lower(), "application/octet-stream")

  # Usage
  files = ["video1.mp4", "video2.mp4", "notes.pdf"]
  submission_id = batch_submit_files(files)
  ```
</CodeGroup>

## Status transitions

A submission moves through the statuses below. The terminal states are `complete`, `partial`, and `failed`.

```mermaid theme={null}
graph LR
  uploading --> submitted
  submitted --> queued
  queued --> processing
  processing --> complete
  processing --> partial
  processing --> failed
```

| Status       | Meaning                                         | Action                                                     |
| ------------ | ----------------------------------------------- | ---------------------------------------------------------- |
| `uploading`  | Assets are being received                       | Wait                                                       |
| `submitted`  | Submission registered and queued                | Processing will start shortly                              |
| `queued`     | Admitted but waiting for a free processing slot | Keep polling — processing starts as slots free up          |
| `processing` | Assets are being analyzed                       | Poll for updates                                           |
| `complete`   | All assets finished successfully                | Retrieve results                                           |
| `partial`    | Some assets succeeded and some failed           | Retrieve successful results; check and retry failed assets |
| `failed`     | Every asset failed                              | Check `errorMessage` / per-asset fields                    |

There is no `created`, `completed`, or `processed` status. Use `complete` for the all-success terminal state and `partial` for a mixed outcome.

## Error handling

If submission creation fails, check these common cases.

### Invalid blob path (403)

```json theme={null}
{ "detail": "..." }
```

Ensure the file was uploaded successfully and the `blobPath` matches the value returned by the upload endpoint exactly.

### Previous submission not found (404)

```json theme={null}
{ "detail": "..." }
```

The `previousSubmissionId` does not resolve to a submission in your workspace.

### Asset already submitted (409)

```json theme={null}
{ "detail": "..." }
```

The asset at that `blobPath` has already been submitted. Use versioning with `previousSubmissionId` to create a new version.

### Request validation failed (422)

```json theme={null}
{
  "error": "validation_error",
  "message": "Request payload is invalid.",
  "fields": [
    { "field": "assets", "message": "..." }
  ]
}
```

Fix the listed fields and resubmit. Common causes:

* **`sidekickIds` missing or empty** — at least one sidekick is required (see [Choosing sidekicks](#choosing-sidekicks)).
* **A per-asset `sidekickIds` is not a subset** of the submission-level `sidekickIds`.
* **An asset's type has no compatible sidekick** — e.g. a PDF submitted only with a video sidekick.

## Best practices

* **Upload with presigned URLs** — request a URL per file and `PUT` the bytes straight to Azure Blob Storage.
* **Batch related assets** — group related files into one submission.
* **Version explicitly** — use `previousSubmissionId` rather than re-submitting the same path.
* **Handle retries** — implement exponential backoff for transient failures.
* **Log submission IDs** — save the `submissionId` for audit trails and support.
