Skip to main content
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 Supported File Formats
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