Skip to content

Large Payload Lane

Guides

Large Payload Lane

Send files above the 4.5 MB direct limit: client-side AES-256-GCM encryption and one-time private staging.

Preview

Preview

The Large Payload Lane ships as a preview and becomes generally available in v1.1.

Tokentide accepts request bodies up to 4.5 MB directly (a limit of the hosting platform; streamed responses and binary media can be any size, non-streaming JSON responses up to 64 MB, see Rate limits). Venice accepts files up to 25 MB. For inputs between the two, use one of these options, in order of preference:

  1. Public URL inputs. For /v1/image/edit, /v1/image/multi-edit, /v1/image/background-remove and style_references, pass a public https:// URL instead of the bytes. Venice fetches it directly (no redirects, at most 25 MB), so the bytes never transit Tokentide.
  2. Encrypted ephemeral staging (this page): for /v1/audio/transcriptions and image files that have no URL. You encrypt the file on your side, upload the ciphertext to a one-time private slot, and reference it in the request.
  3. Compression. Most transcription inputs fit under 4.5 MB when compressed: Opus at 24 kbps is about 3 minutes of speech per MB.

A body above the direct limit answers 413 payload_too_large, and the message points here.

How it works

Large Payload Lane, step by step
  1. 1.Your client: Encrypt the file with a random key K (AES-256-GCM)

  2. 2.Your client → Tokentide API: POST /v1/uploads with purpose, bytes and sha256

  3. 3.Tokentide API → Your client: upload_id, upload_url, headers, expires_at

  4. 4.Your client → Private storage: PUT the ciphertext to upload_url

  5. 5.Your client → Tokentide API: Inference call with X-Tokentide-Upload: upload_id; key=K

  6. 6.Tokentide API → Private storage: Read the ciphertext

  7. 7.Tokentide API: Verify sha256, decrypt in memory

  8. 8.Tokentide API → Venice: Forward the plaintext as the multipart file part

  9. 9.Venice → Tokentide API: Result

  10. 10.Tokentide API → Private storage: Delete the ciphertext

  11. 11.Tokentide API → Your client: Result

  • The ciphertext goes straight from your client to private storage. It never passes through Tokentide’s functions on the way up.
  • The key K reaches Tokentide only in the request header, over TLS. It is used in memory and never stored or logged, so ciphertext left behind is useless.
  • The object is deleted right after Venice responds. A sweeper deletes anything older than 15 minutes.
  • An upload slot is single use, valid for 10 minutes, and usable only by the account that created it. A request that fails with it (wrong key, checksum mismatch) uses it up too.
  • Each account can request 20 slots per 10 minutes, and only while it has a positive balance.

Encryption format

The format is fixed and shared by the TypeScript and Python SDK helpers:

ItemValue
CipherAES-256-GCM, 128-bit authentication tag
Key K32 random bytes (256 bits), new for every upload
Nonce12 random bytes (96 bits), new for every upload
Additional data (AAD)none (empty)
Ciphertext framingnonce (12 bytes) ‖ ciphertext ‖ tag (16 bytes)
Sizeplaintext size + 28 bytes
Header key encodingStandard base64 (RFC 4648 §4) of the 32-byte K, with padding (44 characters)

The nonce travels inside the framed ciphertext, so the header only carries K.

Framed ciphertext
offset 0          12                                   n − 16          n
       ├──nonce───┼──────────── ciphertext ─────────────┼──── tag ─────┤

Step by step

  1. Encrypt locally

    TypeScript (Web Crypto)
    const key = crypto.getRandomValues(new Uint8Array(32));
    const nonce = crypto.getRandomValues(new Uint8Array(12));
    const aesKey = await crypto.subtle.importKey('raw', key, 'AES-GCM', false, ['encrypt']);
    // Web Crypto returns ciphertext ‖ tag; prepend the nonce to get the framed object.
    const sealed = new Uint8Array(
      await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce, tagLength: 128 }, aesKey, plaintext),
    );
    const framed = new Uint8Array(12 + sealed.length);
    framed.set(nonce, 0);
    framed.set(sealed, 12);
  2. Request an upload slot

    bytes and sha256 describe the ciphertext you will upload, never the plaintext: a hash of the plaintext would be a fingerprint of your file.

    curl
    curl https://api.tokentide.app/v1/uploads \
      -H "Authorization: Bearer $TOKENTIDE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "purpose": "audio_transcription",
        "bytes": 10000028,
        "sha256": "3f7a…(64 lowercase hex characters)…",
        "filename": "meeting.m4a",
        "content_type": "audio/mp4"
      }'
    FieldRequiredMeaning
    purposeyesaudio_transcription, image_edit, image_multi_edit, image_upscale or image_background_remove
    bytesyesSize of the framed ciphertext. At most 25 MiB of plaintext (+ 28 bytes)
    sha256yesSHA-256 of the framed ciphertext, lowercase hex
    filenamenoName used for the multipart file part sent to Venice (letters, digits, ., -, _, spaces)
    content_typenoMedia type of the plaintext (for example audio/mp4), used for the file part. Default application/octet-stream
    200 OK
    {
      "object": "upload",
      "upload_id": "upl_01JDV8K3M4N5P6Q7R8S9T0V1W2",
      "upload_url": "https://…",
      "method": "PUT",
      "headers": { "content-type": "application/octet-stream" },
      "expires_at": "2026-10-31T18:10:00.000Z",
      "max_bytes": 10000028,
      "framing": "nonce(12) || ciphertext || tag(16), AES-256-GCM"
    }
  3. Upload the ciphertext

    PUT the framed bytes to upload_url with the returned headers, before expires_at:

    curl
    curl -X PUT "$UPLOAD_URL" -H "content-type: application/octet-stream" --data-binary @meeting.m4a.enc
  4. Call the endpoint with the upload reference

    Send the request as usual, but leave out the file and add X-Tokentide-Upload:

    Request header
    X-Tokentide-Upload: upl_01JDV8K3M4N5P6Q7R8S9T0V1W2; key=q3Zk…(44 base64 characters)…=
    curl
    curl https://api.tokentide.app/v1/audio/transcriptions \
      -H "Authorization: Bearer $TOKENTIDE_API_KEY" \
      -H "X-Tokentide-Upload: $UPLOAD_ID; key=$KEY_BASE64" \
      -F model=openai/whisper-large-v3 \
      -F response_format=json

    Tokentide reads the ciphertext, checks it against the declared sha256, decrypts it in memory, forwards it to Venice as the multipart file part, and deletes it as soon as Venice responds. To parse the header yourself, split on the first ;, then on the first = after key (base64 can end with =).

With the SDK helpers

The SDKs do steps 1–3 for you and return the header to send. The other request fields can be sent as JSON: Tokentide turns them into multipart fields and adds the decrypted file part.

import { readFile } from 'node:fs/promises';
import { TokentideClient, largePayload } from '@tokentide/sdk';
 
const client = new TokentideClient({ apiKey: process.env.TOKENTIDE_API_KEY });
const upload = await largePayload(client, await readFile('meeting.m4a'), {
  purpose: 'audio_transcription',
  filename: 'meeting.m4a',
  contentType: 'audio/mp4',
});
 
const res = await client.inference<{ text: string }>(
  '/audio/transcriptions',
  { model: 'openai/whisper-large-v3', response_format: 'json' },
  { upload: upload.headerValue },
);
console.log(res.data.text, res.meta.chargeUsd);

The Python helper needs the optional cryptography package: pip install "tokentide[crypto]". Multipart works too (send every field except the file part, plus upload.headers), for example with curl as in step 4. See SDK helpers for the full API.

Errors

ResponseCause
400 invalid_requestInvalid slot request; malformed X-Tokentide-Upload header or a key that is not 32 bytes; checksum mismatch; the key does not decrypt the upload; an upload created for another purpose than the endpoint, or sent to an endpoint that takes no upload (the slot is used up)
402 insufficient_balanceThe account has no balance to spend: top up before requesting a slot
404 not_foundUnknown, expired or already used upload, an upload created by another account, or a slot whose PUT has not completed
413 payload_too_largeThe declared size is over the limit for the purpose, the uploaded object is larger than declared, or the other request fields exceed 64 KiB next to a file at the 25 MiB limit
429 rate_limitedMore than 20 slots requested in 10 minutes (per account) or about 30 per minute from your network; honor Retry-After
503 service_unavailableThe Large Payload Lane is not enabled on this deployment: use a public URL input instead

Request a new slot after any failure; slots are never reused.