Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.adclear.ai/llms.txt

Use this file to discover all available pages before exploring further.

Text promotions skip this step entirely. If your content is plain text or HTML, provide it inline via the content field when creating a promotion.
Uploads use a resumable chunked protocol. You initiate the upload, then send the file in one or more chunks using Content-Range headers.
1

Initiate the upload

Request:
POST https://files.adclear.ai/v1/uploads

{
  "fileName": "summer-campaign-v1.pdf",
  "fileSize": 2048576,
  "contentType": "application/pdf"
}
Response:
{
  "uploadId": "a1b2c3d4-...",
  "uploadUrl": "/v1/uploads/a1b2c3d4-...",
  "maxChunkSize": 52428800
}
  • uploadUrl is the relative path for chunk uploads.
  • maxChunkSize is the maximum bytes per chunk (default 50 MB). The final chunk may be smaller.
2

Upload the file in chunks

Send the file bytes as one or more PUT requests with a Content-Range header.Single chunk (file smaller than maxChunkSize):
PUT https://files.adclear.ai/v1/uploads/{uploadId}
Content-Type: application/pdf
Content-Range: bytes 0-2048575/2048576

<binary file data>
Response (200 OK):
{
  "uploadId": "a1b2c3d4-...",
  "status": "completed",
  "bytesReceived": 2048576
}
Multiple chunks (large file):
PUT https://files.adclear.ai/v1/uploads/{uploadId}
Content-Range: bytes 0-52428799/157286400
-> 200 { "status": "uploading", "bytesReceived": 52428800 }

PUT https://files.adclear.ai/v1/uploads/{uploadId}
Content-Range: bytes 52428800-104857599/157286400
-> 200 { "status": "uploading", "bytesReceived": 104857600 }

PUT https://files.adclear.ai/v1/uploads/{uploadId}
Content-Range: bytes 104857600-157286399/157286400
-> 200 { "status": "completed", "bytesReceived": 157286400 }
Content-Range format: bytes <start>-<end>/<total> where start and end are zero-based inclusive byte offsets and total is the full file size.
3

Check upload status (optional)

You can poll upload status at any time:
GET https://files.adclear.ai/v1/uploads/{uploadId}
{
  "uploadId": "a1b2c3d4-...",
  "status": "completed",
  "fileName": "summer-campaign-v1.pdf",
  "fileSize": 2048576,
  "createdAt": "2026-02-16T12:00:00.000Z"
}
Status transitions: pending -> uploading -> completed | failed.
Implementation notes:
  • Multiple files can be uploaded in parallel. Hold onto all uploadId values, as you’ll reference them when creating the promotion.
  • If a chunk fails due to a network error, check the upload status to see how many bytes were received, then resume from that offset.
  • Uploading to an already-completed upload returns 409 Conflict.

Code Example (TypeScript)

Below is a self-contained TypeScript example that demonstrates the full upload flow, including chunked uploads for large files. Run with npx tsx upload.ts <file-path> [content-type].
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';

const BASE_URL = 'https://files.adclear.ai';
const API_KEY = process.env.ADCLEAR_API_KEY!;

const authHeaders = {
  Authorization: `Bearer ${API_KEY}`,
  'Content-Type': 'application/json'
};

// Step 1 -- Initiate the upload
async function initiateUpload(
  filePath: string,
  contentType: string
): Promise<{ uploadId: string; maxChunkSize: number }> {
  const fileBuffer = await readFile(filePath);

  const res = await fetch(`${BASE_URL}/v1/uploads`, {
    method: 'POST',
    headers: authHeaders,
    body: JSON.stringify({
      fileName: basename(filePath),
      fileSize: fileBuffer.byteLength,
      contentType
    })
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Initiate failed (${res.status}): ${err.error.message}`);
  }

  const { uploadId, maxChunkSize } = await res.json();
  return { uploadId, maxChunkSize };
}

// Step 2 -- Upload the file in chunks
async function uploadChunks(
  filePath: string,
  uploadId: string,
  contentType: string,
  maxChunkSize: number
): Promise<void> {
  const fileBuffer = await readFile(filePath);
  const totalSize = fileBuffer.byteLength;
  let offset = 0;

  while (offset < totalSize) {
    const end = Math.min(offset + maxChunkSize, totalSize) - 1;
    const chunk = fileBuffer.subarray(offset, end + 1);

    const res = await fetch(`${BASE_URL}/v1/uploads/${uploadId}`, {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': contentType,
        'Content-Range': `bytes ${offset}-${end}/${totalSize}`
      },
      body: chunk
    });

    if (!res.ok) {
      const err = await res.json();
      throw new Error(`Chunk upload failed (${res.status}): ${err.error.message}`);
    }

    const body = await res.json();
    console.log(`Uploaded bytes ${offset}-${end} of ${totalSize} -- status: ${body.status}`);
    offset = end + 1;
  }
}

// Step 3 -- Verify the upload completed
async function getUploadStatus(uploadId: string): Promise<{ status: string }> {
  const res = await fetch(`${BASE_URL}/v1/uploads/${uploadId}`, {
    headers: { Authorization: `Bearer ${API_KEY}` }
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Status check failed (${res.status}): ${err.error.message}`);
  }

  return res.json();
}

// Full example
async function main(): Promise<void> {
  const filePath = process.argv[2];
  const contentType = process.argv[3] ?? 'application/pdf';

  if (!filePath) {
    console.error('Usage: npx tsx upload.ts <file-path> [content-type]');
    process.exit(1);
  }

  const { uploadId, maxChunkSize } = await initiateUpload(filePath, contentType);
  console.log(`Upload initiated: ${uploadId} (chunk size: ${maxChunkSize} bytes)`);

  await uploadChunks(filePath, uploadId, contentType, maxChunkSize);

  const status = await getUploadStatus(uploadId);
  console.log(`Final status: ${status.status}`);
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});

Common Error Scenarios

ScenarioHTTP StatusError CodeWhat to do
File too large for its format413PAYLOAD_TOO_LARGECheck file size limits in Getting started → Overview
Missing Content-Range header400VALIDATION_ERRORAdd Content-Range: bytes <start>-<end>/<total>
Chunk body size does not match range span400VALIDATION_ERROREnsure byte length of body equals end - start + 1
Content-Range total does not match declared fileSize400VALIDATION_ERRORUse the same total in every chunk as the fileSize from initiation
Upload already completed409INVALID_REQUESTThe file is already uploaded; proceed to create the promotion
Upload not found404NOT_FOUNDThe upload ID is invalid or the session expired; initiate a new upload
Network failure mid-uploadN/AN/AGET the upload status to check bytesReceived, then resume from that offset
For cross-cutting errors (401, 403, 429, 502), see Getting started → Errors.

Frequently asked questions

Yes. The uploadIds field on the promotion accepts an array of up to 10 upload IDs. Upload each file separately, then pass all IDs when creating the promotion.
{
  "uploadIds": ["upload-1-uuid", "upload-2-uuid", "upload-3-uuid"],
  "fileFormat": "pdf"
}
Each file is evaluated independently in parallel. The evaluation response returns per-file results in the results array.
Yes. Upload IDs aren’t consumed. The same upload can be referenced by multiple promotions or versions.