> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tryordinal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# File Uploads

> Upload media to Ordinal from a public URL or a local file, then attach the returned assetId when creating posts via the API.

## Overview

The Ordinal API supports two ways to upload media:

* **URL upload** — submit a publicly accessible URL and Ordinal downloads the file for you.
* **Local file upload** — request signed credentials, then upload the file directly to the returned `uploadUrl`.

Both flows are asynchronous. You receive an upload job ID, poll until the status is `ready`, then use the returned `assetId` when [creating posts](/api-reference/posts/create-post).

## Choosing a flow

| Use case                                                      | Endpoint                                      |
| ------------------------------------------------------------- | --------------------------------------------- |
| The file is already hosted at a public URL                    | [`POST /uploads`](#url-upload)                |
| The file lives on your machine, server, or in private storage | [`POST /uploads/prepare`](#local-file-upload) |

## Supported file types

| Type   | Formats              |
| ------ | -------------------- |
| Images | JPEG, PNG, GIF, WebP |
| Videos | MP4, QuickTime (MOV) |

## File size limits

| Type   | Max size |
| ------ | -------- |
| Images | 10 MB    |
| GIFs   | 15 MB    |
| Videos | 350 MB   |

<Note>
  Images cannot exceed 36 megapixels total resolution.
</Note>

## Upload statuses

| Status            | Description                                                                        |
| ----------------- | ---------------------------------------------------------------------------------- |
| `awaiting_upload` | Credentials issued for a local file. Ordinal is waiting for the file to be POSTed. |
| `pending`         | Upload is queued and waiting to be processed                                       |
| `processing`      | File is being downloaded and processed                                             |
| `ready`           | Upload is complete. Use the `assetId` to attach to posts                           |
| `failed`          | Upload failed. Check the `error` field for details                                 |
| `expired`         | Upload expired before being used in a post                                         |

## URL upload

Use this flow when the file is already hosted somewhere Ordinal can reach over the public internet.

<Steps>
  <Step title="Submit the file URL">
    [POST to `/uploads`](/api-reference/file-uploads/upload-a-file-from-a-url) with the publicly accessible URL of the file.
  </Step>

  <Step title="Receive an upload job ID">
    The API returns an upload job ID with `pending` status.
  </Step>

  <Step title="Poll for status">
    [Poll `GET /uploads/{id}`](/api-reference/file-uploads/get-upload-status) until the status changes to `ready`.
  </Step>

  <Step title="Use the asset ID">
    Once ready, use the returned `assetId` in the `assetIds` array when creating posts.
  </Step>
</Steps>

### Create a URL upload

```bash theme={null}
curl -X POST "https://app.tryordinal.com/api/v1/uploads" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/images/product-photo.jpg"}'
```

#### Response

```json theme={null}
{
  "id": "d4f8e2a1-3b7c-4e9d-8f2a-1c5b9e7d3a6f",
  "status": "pending",
  "createdAt": "2026-02-04T10:30:00.000Z"
}
```

<Warning>
  The file URL must be publicly accessible. URLs that require authentication or sit behind a firewall will fail.
</Warning>

## Local file upload

Use this flow when the file is on disk, in private storage, or otherwise not reachable from the public internet. Ordinal returns signed credentials that let your client upload the bytes directly to the returned `uploadUrl`.

<Steps>
  <Step title="Prepare the upload">
    [POST to `/uploads/prepare`](/api-reference/file-uploads/prepare-a-local-file-upload) with the file's `filename`, `mimetype`, and `size` in bytes.
  </Step>

  <Step title="Upload the file">
    POST the file to the returned `uploadUrl` as `multipart/form-data` with three form fields: `params` (the JSON string from the prepare response), `signature` (the signature from the prepare response), and `file` (the local file bytes). Copy `params` and `signature` verbatim — do not modify them. Ordinal is notified automatically when processing finishes.
  </Step>

  <Step title="Poll for status">
    [Poll `GET /uploads/{id}`](/api-reference/file-uploads/get-upload-status) until the status changes to `ready`.
  </Step>

  <Step title="Use the asset ID">
    Once ready, use the returned `assetId` in the `assetIds` array when creating posts.
  </Step>
</Steps>

### Prepare the upload

```bash theme={null}
curl -X POST "https://app.tryordinal.com/api/v1/uploads/prepare" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "product-photo.jpg",
    "mimetype": "image/jpeg",
    "size": 245678
  }'
```

#### Response

```json theme={null}
{
  "id": "d4f8e2a1-3b7c-4e9d-8f2a-1c5b9e7d3a6f",
  "status": "awaiting_upload",
  "uploadUrl": "https://upload.example.com",
  "params": "{\"auth\":{\"key\":\"...\",\"expires\":\"2026-02-04T11:30:00.000Z\"},\"template_id\":\"...\",\"fields\":{\"uploadJobId\":\"...\",\"workspaceId\":\"...\",\"storagePath\":\"...\"}}",
  "signature": "sha384:abc123...",
  "expiresAt": "2026-02-04T11:30:00.000Z",
  "createdAt": "2026-02-04T10:30:00.000Z"
}
```

The `params` value is a signed JSON string. It includes `auth` (with `key` and `expires`), `template_id`, and `fields` (`uploadJobId`, `workspaceId`, `storagePath`). The credentials are short‑lived — upload the file before `expiresAt`.

### Upload the file

POST the file to `uploadUrl` as `multipart/form-data` with three form fields. Use the `params` and `signature` values from the prepare response exactly as returned — do not parse, reformat, or regenerate them.

| Form field  | Value                                            |
| ----------- | ------------------------------------------------ |
| `params`    | The `params` string from the prepare response    |
| `signature` | The `signature` string from the prepare response |
| `file`      | The local file bytes                             |

```bash theme={null}
curl -X POST "https://upload.example.com" \
  -F 'params={"auth":{"key":"...","expires":"2026-02-04T11:30:00.000Z"},"template_id":"...","fields":{"uploadJobId":"...","workspaceId":"...","storagePath":"..."}}' \
  -F 'signature=sha384:abc123...' \
  -F "file=@./video.mp4"
```

In practice, substitute `uploadUrl`, `params`, and `signature` with the values returned by `/uploads/prepare`.

Here's the same step in Node.js:

```javascript theme={null}
import fs from "node:fs";

const form = new FormData();
form.append("params", prepare.params);
form.append("signature", prepare.signature);
form.append("file", new Blob([fs.readFileSync("./video.mp4")]), "video.mp4");

await fetch(prepare.uploadUrl, { method: "POST", body: form });
```

You don't need to do anything else after the upload completes — Ordinal is notified automatically and transitions the job to `processing` and then `ready`.

## Checking upload status

Poll the upload endpoint to check when processing is complete:

```bash theme={null}
curl -X GET "https://app.tryordinal.com/api/v1/uploads/d4f8e2a1-3b7c-4e9d-8f2a-1c5b9e7d3a6f" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Ready response

When the upload is ready, the response includes the `assetId` and file metadata:

```json theme={null}
{
  "id": "d4f8e2a1-3b7c-4e9d-8f2a-1c5b9e7d3a6f",
  "status": "ready",
  "assetId": "a7c2e4b9-5d1f-4a8e-9c3b-2f6d8e1a4b7c",
  "filename": "product-photo.jpg",
  "mimetype": "image/jpeg",
  "size": 245678,
  "width": 1920,
  "height": 1080,
  "duration": null,
  "expiresAt": "2026-02-11T10:30:00.000Z",
  "createdAt": "2026-02-04T10:30:00.000Z",
  "readyAt": "2026-02-04T10:30:15.000Z"
}
```

### Failed response

If the upload fails, the response includes an error message:

```json theme={null}
{
  "id": "d4f8e2a1-3b7c-4e9d-8f2a-1c5b9e7d3a6f",
  "status": "failed",
  "error": "Failed to download file: HTTP 404",
  "createdAt": "2026-02-04T10:30:00.000Z"
}
```

## Using assets in posts

Include the `assetId` in the `assetIds` array when creating a post:

```json theme={null}
{
  "title": "Product Launch",
  "publishAt": "2026-02-10T14:00:00.000Z",
  "status": "Scheduled",
  "linkedIn": {
    "profileId": "b3e7f1c9-2d4a-4f8b-a6c1-9e5d7b2f8a3c",
    "copy": "Check out our new product!",
    "assetIds": ["a7c2e4b9-5d1f-4a8e-9c3b-2f6d8e1a4b7c"]
  }
}
```

### Platform attachment limits

| Platform    | Max attachments |
| ----------- | --------------- |
| X (Twitter) | 4               |
| LinkedIn    | 20              |

## Important notes

<Note>
  Uploads expire within 24 hours if not used in a post. Once an upload expires, create a new upload job.
</Note>

<Note>
  For videos, the `duration` field in the ready response contains the video length in seconds. For images, this field is `null`.
</Note>
