> ## 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.

# Sandbox environment

> Test your integration safely using a sandbox API key

## Overview

The sandbox lets you exercise your integration without running real asset processing or consuming processing credits. It is not a separate host or base URL. You select the sandbox by using a sandbox API key: the same base URL and the same paths behave as a sandbox when the request is authenticated with an `sk_test_` key.

Sandbox keys unlock two capabilities:

* A `simulationDelaySeconds` query parameter on submission creation, to control timing.
* A `POST /api/integrations/sandbox/events/fire` endpoint, to dispatch synthetic webhook events to your subscriptions without real processing.

<Note>
  Sandbox submissions are excluded from [Insights](/concepts/insights) — their canned results never affect your workspace's counts, rates, or processing durations.
</Note>

## Using a sandbox key

Use your `sk_test_` key against the normal base URL and the normal endpoints. The asset filename selects the outcome to simulate — see [Choosing a test scenario](#choosing-a-test-scenario). As with live submissions, `sidekickIds` is required (at least one), though the sandbox does not actually run the named sidekicks.

```bash theme={null}
curl -X POST https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions \
  -H "X-API-Key: sk_test_your_sandbox_key" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [
      { "blobPath": "11111111-1111-1111-1111-111111111111/__sandbox_pass__" }
    ],
    "sidekickIds": ["<sidekick-id>"]
  }'
```

A live (`sk_live_`) key calls the same endpoints but cannot use the sandbox-only features described below.

## Choosing a test scenario

Sandbox submissions never run real analysis. Instead, you pick the outcome by embedding a **magic marker** in the asset's filename. The marker selects a canned result that flows through the normal read endpoints — submission status, per-asset issues, and retry — exactly as a real processing result would.

<Warning>
  Every asset in a sandbox submission **must** contain one of the markers below in its filename. A sandbox submission whose asset filename has no recognised marker is rejected with `400 Bad Request`:

  ```json theme={null}
  { "detail": "Sandbox API keys only accept magic filenames. '<filename>' must contain one of: __sandbox_pass__, __sandbox_advisory__, __sandbox_fail__, __sandbox_error__, __sandbox_retryable_error__, __sandbox_pending__. See the sandbox documentation for details." }
  ```

  Live (`sk_live_`) keys ignore these markers entirely — they run real processing regardless of filename.
</Warning>

`blobPath` always has the form `{asset-id}/{filename}`, where `{asset-id}` is a UUID. The marker goes in the **filename** — the part after the `/`. The simplest form is to use the marker as the whole filename, e.g. `11111111-1111-1111-1111-111111111111/__sandbox_pass__`.

Because the sandbox never reads a real file, you do not need to upload anything first for sandbox submissions — any UUID works as the asset id. (For live submissions you obtain `blobPath` from the [upload endpoints](/guides/file-upload) instead.)

| Filename marker               | Submission status | Asset outcome                         | Issues produced                 | Retryable |
| ----------------------------- | ----------------- | ------------------------------------- | ------------------------------- | --------- |
| `__sandbox_pass__`            | `complete`        | Clean pass                            | None                            | —         |
| `__sandbox_advisory__`        | `complete`        | Passes with advisories                | 2 — one `minor`, one `info`     | —         |
| `__sandbox_fail__`            | `complete`        | Completes but carries blocking issues | 3 — one `critical`, two `major` | —         |
| `__sandbox_error__`           | `failed`          | Non-retryable processing failure      | None                            | No        |
| `__sandbox_retryable_error__` | `failed`          | Transient processing failure          | None                            | Yes       |
| `__sandbox_pending__`         | `processing`      | Stays in processing, never finalises  | None                            | —         |

A few details worth noting:

* **`__sandbox_fail__` still reports `complete`.** A "fail" here means the asset finished but surfaced critical/major issues — the submission status is `complete` and the issues appear via the asset issues endpoint. Use it to exercise issue-handling logic.
* **`__sandbox_error__` vs `__sandbox_retryable_error__`.** Both drive the submission to `failed`. The error variant is a permanent failure (`isRetryable: false`); the retryable variant sets `isRetryable: true` so you can exercise the [retry endpoint](/concepts/submissions#error-handling). The submission's `errorMessage` is `Sandbox simulated error` or `Sandbox simulated retryable error` respectively.
* **`__sandbox_pending__`** leaves the submission in `processing` indefinitely — useful for testing polling timeouts and real-time status updates.
* **Mixed-scenario submissions.** When a submission contains assets with different markers, the worst outcome decides the overall submission status, in this order (worst first): `error` → `retryable_error` → `fail` → `advisory` → `pass` → `pending`. Each asset still carries its own issues and per-asset failure fields.

### Example: testing the issue path

```bash theme={null}
curl -X POST https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions \
  -H "X-API-Key: sk_test_your_sandbox_key" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [
      { "blobPath": "11111111-1111-1111-1111-111111111111/__sandbox_fail__" }
    ],
    "sidekickIds": ["<sidekick-id>"]
  }'
```

Poll the submission to completion, then read the issues for the returned asset — you will get one `critical` and two `major` issues to drive your review UI or compliance checks.

### Example: testing the retry path

```bash theme={null}
curl -X POST https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions \
  -H "X-API-Key: sk_test_your_sandbox_key" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [
      { "blobPath": "11111111-1111-1111-1111-111111111111/__sandbox_retryable_error__" }
    ],
    "sidekickIds": ["<sidekick-id>"]
  }'
```

The submission ends as `failed` with `isRetryable: true`. Call the [retry endpoint](/concepts/submissions#error-handling) for that asset to exercise your recovery flow: the sandbox simulates a successful retry, so the asset recovers and the submission moves to `complete` with `isRetryable: false`. The retry is single-use — once the asset has recovered it is no longer in a failed state, so a second retry returns `409`. Submit `__sandbox_error__` instead to verify that a non-retryable failure is rejected by the retry endpoint with `409`.

## Simulating processing time

Add the `simulationDelaySeconds` query parameter to a submission-create request to delay when the submission reaches a terminal state. This is useful for exercising your polling and webhook-handling logic.

```bash theme={null}
curl -X POST "https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions?simulationDelaySeconds=5" \
  -H "X-API-Key: sk_test_your_sandbox_key" \
  -H "Content-Type: application/json" \
  -d '{ "assets": [ { "blobPath": "11111111-1111-1111-1111-111111111111/__sandbox_pass__" } ] }'
```

* The parameter is camelCase: `simulationDelaySeconds`.
* Accepted range is 0 to 30 seconds. Default is 0.
* It works only with a sandbox key. Sending it on a live key returns a 400.

## Firing synthetic webhook events

To test your webhook handler end to end, fire a synthetic event for an existing submission's assets. The endpoint dispatches the event to every matching subscription in your workspace, exactly as a real processing event would, but without running any processing.

### POST /api/integrations/sandbox/events/fire

Request body (camelCase):

```json theme={null}
{
  "eventType": "asset.processing.failed",
  "submissionId": "550e8400-e29b-41d4-a716-446655440000",
  "errorMessage": "Simulated processing failure"
}
```

| Field          | Required | Notes                                                            |
| -------------- | -------- | ---------------------------------------------------------------- |
| `eventType`    | Yes      | One of `asset.processing.completed` or `asset.processing.failed` |
| `submissionId` | Yes      | A submission that exists in your workspace                       |
| `errorMessage` | No       | Optional message, used with `asset.processing.failed`            |

Response 200:

```json theme={null}
{
  "dispatched": 2
}
```

`dispatched` is the number of subscriptions notified across all of the submission's assets.

This endpoint is sandbox-only. Behaviour by condition:

| Condition                                  | Result                      |
| ------------------------------------------ | --------------------------- |
| Sandbox key, submission in workspace       | 200 with `dispatched` count |
| Live key                                   | 403                         |
| `submissionId` not found in your workspace | 404                         |
| Malformed request body                     | 422                         |

## Testing the failure path

There are two complementary ways to exercise failure handling in the sandbox. To test the **processing-result read path**, submit an asset with the `__sandbox_error__` or `__sandbox_retryable_error__` marker (see [Choosing a test scenario](#choosing-a-test-scenario)) and read the failure back from the submission status. To test **webhook delivery** end to end, subscribe a webhook, create a submission, then fire an `asset.processing.failed` event and confirm your endpoint received it.

```python theme={null}
import requests

API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"
api_key = "sk_test_your_sandbox_key"


def test_failure_webhook(submission_id: str) -> None:
    """Subscribe, fire a synthetic failure event, and confirm dispatch."""
    # 1. Subscribe to the failure event.
    sub = requests.post(
        f"{API_BASE}/api/integrations/webhooks/subscriptions",
        headers={"X-API-Key": api_key},
        json={
            "endpointUrl": "https://your-domain.com/webhooks/test",
            "eventTypes": ["asset.processing.failed"],
            "description": "Sandbox failure test",
        },
    )
    secret = sub.json()["secret"]
    print(f"Subscribed; verify deliveries with secret {secret}")

    # 2. Fire a synthetic failure for an existing submission's assets.
    fired = requests.post(
        f"{API_BASE}/api/integrations/sandbox/events/fire",
        headers={"X-API-Key": api_key},
        json={
            "eventType": "asset.processing.failed",
            "submissionId": submission_id,
            "errorMessage": "Simulated processing failure",
        },
    )

    dispatched = fired.json()["dispatched"]
    assert dispatched >= 1, "expected at least one subscription to be notified"
    print(f"Dispatched failure event to {dispatched} subscription(s)")
    print("Now assert your endpoint received an asset.processing.failed event")
```

The delivered webhook payload follows the standard shape documented in [Webhook subscriptions](/guides/webhook-subscriptions): an outer `type` of `asset.processing.failed`, with a snake\_case `data` object containing `asset_id`, `submission_id`, `workspace_id`, `status` of `"failed"`, and `error_message`.

## Key differences from production

| Aspect             | Sandbox (`sk_test_` key)                                                | Production (`sk_live_` key)       |
| ------------------ | ----------------------------------------------------------------------- | --------------------------------- |
| Selection          | API key prefix `sk_test_`                                               | API key prefix `sk_live_`         |
| Base URL and paths | Same as production                                                      | Same as sandbox                   |
| Processing         | No real analysis or credits consumed; outcome chosen by filename marker | Real processing                   |
| Asset filename     | Must contain a `__sandbox_*__` scenario marker                          | Real filename, no marker required |
| Webhook events     | Can be fired synthetically via `/sandbox/events/fire`                   | Emitted by real processing        |
| Timing control     | `simulationDelaySeconds` (0–30) on submission create                    | Not available                     |

## Best practices

<Tip>
  * Use a sandbox key for all integration testing before switching to a live key.
  * Pick the outcome you want to test with the filename marker — `__sandbox_fail__` for issue handling, `__sandbox_retryable_error__` for retries, `__sandbox_pending__` for polling timeouts.
  * Exercise your webhook handler by firing `asset.processing.failed` against an existing submission.
  * Use `simulationDelaySeconds` to test polling and webhook timing.
  * Verify webhook signature handling against the secret returned when you create a subscription.
</Tip>

## Troubleshooting

### Sandbox submission rejected with 400

* A sandbox key requires every asset filename to contain a scenario marker (e.g. `__sandbox_pass__`). Add one of the markers from [Choosing a test scenario](#choosing-a-test-scenario) to the filename portion of `blobPath`.
* The marker must appear after the first `/` in `blobPath` — that is the filename the sandbox inspects.

### Webhook not delivered

* Ensure your endpoint is publicly reachable and returns 200
* Confirm the subscription's `eventTypes` includes the event you fired
* Check that `dispatched` was at least 1 in the fire response
* Review delivery history via the subscription's deliveries endpoint

### simulationDelaySeconds rejected

* The parameter is camelCase: `simulationDelaySeconds`
* The value must be between 0 and 30
* A 400 means it was sent on a live key — use a sandbox (`sk_test_`) key

### Fire endpoint returns 403 or 404

* 403 means you used a live key; sandbox events require a `sk_test_` key
* 404 means the `submissionId` does not exist in your workspace

### Ready for production?

Switch to your live (`sk_live_`) key against the same endpoints. Create or manage keys in the control plane, or contact support at [https://mediamagic-verify.support.site](https://mediamagic-verify.support.site).
