Source: https://developers.tokportal.com/sandbox/
Markdown: https://developers.tokportal.com/sandbox.md

# Sandbox (dry run)

Credits are debited when a bundle is **created**, not when it is published, and there is no cancellation or refund path. That makes the first call to an unfamiliar operation expensive to get wrong.

The sandbox removes that risk. Send one header and any write becomes a **simulation**: the request runs the same validation and the same server-side pricing as the real call, then stops before the first write.

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "X-TokPortal-Dry-Run: true" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "USA",
    "videos_quantity": 3,
    "edits_quantity": 0,
    "wants_advanced_warming": false
  }'
```

```json
{
  "data": {
    "bundle_id": "00000000-0000-4000-8000-6b5f697784b1",
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "USA",
    "status": "pending_setup",
    "videos_quantity": 3,
    "review_mode": "auto",
    "created_at": "2026-09-01T13:43:56.863Z"
  },
  "credits_charged": 0,
  "credits_would_charge": 38,
  "credits_remaining": 11823,
  "cost_breakdown": {
    "account_creation": 32,
    "video_slots": 6,
    "edit_slots": 0,
    "advanced_warming": 0,
    "total": 38
  },
  "dry_run": true,
  "dry_run_notice": "Simulated: nothing was created, no credits were debited, and the returned identifiers do not exist. Repeat this call without the X-TokPortal-Dry-Run header to execute it for real."
}
```

`201 Created`, and your balance is untouched.

## How to ask for it

| Surface    | How                                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| REST       | Header `X-TokPortal-Dry-Run: true` (also accepts `1` and `yes`, case-insensitive)                       |
| MCP        | Top-level tool argument `dry_run: true`; use the remote server or local `tokportal-mcp` 1.15.1 or later |
| SDKs / CLI | Pass the header through your client's request options                                                   |

Any other value, including `false` and `0`, means a real call. So does a missing header.

On `GET` requests the header is **ignored**. Reads are free and change nothing, so a client that sets the header globally keeps working.

### Local MCP version requirement

Local dry runs require **`tokportal-mcp` 1.15.1 or later**. Version 1.15.0 exposes the argument but does not forward it as the dry-run header, so its write tools must not be used for simulations. Use the remote endpoint `https://app.tokportal.com/api/ext/mcp` or REST while upgrading.

For a global installation:

```bash
npm install --global tokportal-mcp@1.15.1
```

For a host configured with `npx`, set its command arguments to `["-y", "tokportal-mcp@1.15.1"]`. Restart the MCP server after changing the installation or configuration. If you cannot upgrade or verify the running version, keep using the remote endpoint or REST; do not fall back to 1.15.0.

## What a dry run returns

Everything a real call returns, in the same shape, with the same HTTP status — plus:

| Field                  | Meaning                                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dry_run`              | Always `true`. **Absent — never `false` — on a real call.**                                                                                                 |
| `dry_run_notice`       | One sentence restating what did and did not happen, for an agent to relay to a person.                                                                      |
| `credits_would_charge` | The real price this call would have cost, your workspace allowance included. This is the number to confirm with a human before repeating the call for real. |
| `credits_charged`      | Always `0`. Nothing was debited, so a client summing its spend must not count simulations.                                                                  |
| `credits_remaining`    | Your real, current, unchanged balance.                                                                                                                      |

Every response — success **and** error — also carries the header `X-TokPortal-Dry-Run: true`. On errors that header is the only marker, so check it there rather than parsing the body.

## Errors are identical, on purpose

A dry run does not soften anything. An invalid request fails with exactly the code, status and `details` it would fail with for real:

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "X-TokPortal-Dry-Run: true" \
  -H "Content-Type: application/json" \
  -d '{"bundle_type":"account_only","platform":"tiktok","country":"USA","advanced_warming_terms":["fitness","gym","protein"]}'
```

```json
{
  "error": {
    "code": "ADVANCED_WARMING_FLAG_REQUIRED",
    "message": "Advanced Niche Warming targets were supplied without opting in. Add \"wants_advanced_warming\": true to the same request body, next to advanced_warming_terms / advanced_warming_terms_count. The flag is mandatory: it is what is priced and what creates the warming session, and TokPortal never infers it. Check \"details.supplied_fields\" for the fields that triggered this.",
    "details": {
      "supplied_fields": ["advanced_warming_terms"],
      "wants_advanced_warming": false,
      "fix": "Re-send the same body with \"wants_advanced_warming\": true added."
    }
  }
}
```

That is the point of the sandbox: learn what an operation refuses **before** a real call refuses it and you have to reason about whether you were charged.

## Chaining a whole flow

Identifiers returned by a dry run are **synthetic**. They are valid UUIDs, they always start with `00000000-0000-4000-8000-`, and they exist only inside dry-run calls — where they are accepted so you can walk an entire flow:

```bash
# 1. Simulate the creation and keep the synthetic id
BUNDLE=00000000-0000-4000-8000-6b5f697784b1

# 2. Simulate configuring its account
curl -X PUT "https://app.tokportal.com/api/ext/bundles/$BUNDLE/account" \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "X-TokPortal-Dry-Run: true" \
  -H "Content-Type: application/json" \
  -d '{"username":"my_brand_01","visible_name":"My Brand","niche":"fitness"}'

# 3. Simulate publishing it
curl -X POST "https://app.tokportal.com/api/ext/bundles/$BUNDLE/publish" \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "X-TokPortal-Dry-Run: true" \
  -H "Content-Type: application/json" -d '{}'
```

Send a synthetic identifier to a **real** call and it is refused immediately, rather than surfacing as a confusing not-found:

```json
{
  "error": {
    "code": "DRY_RUN_ID_IN_LIVE_REQUEST",
    "message": "This identifier was produced by a dry run and does not exist. Re-run the creation call without the X-TokPortal-Dry-Run header to obtain a real one.",
    "details": { "parameter": "id", "value": "00000000-0000-4000-8000-6b5f697784b1" }
  }
}
```

> **NOTE: Simulated objects have no memory**
> A synthetic bundle does not remember the `videos_quantity` you created it with. Each call is validated fully on its own; cross-call consistency is not simulated. Use the sandbox to learn shapes, prices and errors — not to predict how a specific bundle will behave three steps later.

## Guarantees

In dry-run mode, none of this happens:

- No credits debited, and no contract allowance slot consumed — though the price reflects the allowance.
- No row written, in any table.
- No webhook delivered.
- No notification, no SMS, no email: no account manager ever sees a simulated bundle.
- No secret revealed. `revealAccountCredentials` and `retrieveAccountVerificationCode` return their `428` policy preview and nothing else — never a credential, real or invented — and the account's Coverage is untouched.

And this still happens, exactly as in a real call:

- Authentication, key scopes, expiry and revocation.
- Every business validation, in the same order, with the same codes.
- **Rate limits.** A dry run consumes the same bucket as a real call: the server work is real even when the write is not. Do not loop dry runs.

### Your Idempotency-Key survives

An `Idempotency-Key` sent alongside a dry run is **ignored, not consumed**. The real call that follows with the same key executes normally rather than replaying the simulated response.

```bash
# Simulate with a key…
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "Idempotency-Key: launch-2026-09-01-a" \
  -H "X-TokPortal-Dry-Run: true" \
  -H "Content-Type: application/json" -d "$BODY"

# …then execute for real with the SAME key. This creates the bundle.
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: $TOKPORTAL_API_KEY" \
  -H "Idempotency-Key: launch-2026-09-01-a" \
  -H "Content-Type: application/json" -d "$BODY"
```

## From an agent

Every non-`GET` MCP tool takes `dry_run` as a top-level argument:

```json
{
  "name": "tokportal_create_bundle",
  "arguments": {
    "dry_run": true,
    "body": {
      "bundle_type": "account_and_videos",
      "platform": "tiktok",
      "country": "USA",
      "videos_quantity": 3,
      "edits_quantity": 0,
      "wants_advanced_warming": false
    }
  }
}
```

The recommended pattern for an agent acting on someone's behalf: **dry-run first, show the human `credits_would_charge`, then repeat the call for real once they agree.** It costs nothing and removes the only irreversible decision in the flow.

Send `dry_run` as a real JSON boolean: `true` for a simulation, `false` for a live call. Compatible boolean-like inputs such as the string `"true"` are accepted by the parser; ambiguous values are rejected. A missing argument means a live call.

## Related

- [Credits](https://developers.tokportal.com/credits) — what each operation costs and when it is debited
- [Errors](https://developers.tokportal.com/errors) — the full error-code reference
- [Rate limits](https://developers.tokportal.com/rate-limits) — the buckets a dry run also consumes
- [MCP server](https://developers.tokportal.com/mcp) — installing the server that exposes `dry_run` as a tool argument
