---
title: "Bulk upload from CSV"
method: POST
path: "/v1/posts/bulk-upload"
tags: ["Posts"]
---

# Bulk upload from CSV

`POST /v1/posts/bulk-upload`

Create multiple posts by uploading a CSV file. Use dryRun=true to validate without creating posts.

## Query parameters

- `dryRun` boolean

## Response `200`

Bulk upload results. Returned when every row succeeded (or every row failed).
A mix of successes and failures returns `207` instead, with the same body shape.

- BulkUploadResult — Result of a CSV bulk upload. The same shape is returned for `200` (all rows succeeded or all failed) and `207` (mixed). Per-row outcomes live in `results`; the row's success is `ok`, and failures carry machine-readable codes in `errors`.
  - `total` integer — Number of data rows processed from the CSV
  - `valid` integer — Count of rows that succeeded (results[].ok === true)
  - `invalid` integer — Count of rows that failed (total - valid)
  - `results` object[] — One entry per CSV data row, in row order.
    - `rowIndex` integer — 1-based index of the CSV data row (header excluded)
    - `ok` boolean — Whether the row was created successfully
    - `createdPostId` string — ID of the created post. Present only when `ok` is true and not a dry run.
    - `errors` string[] — Machine-readable failure codes for this row. Present only when `ok` is false. Examples: `unknown_profile:<id>`, `no_account_for_platform:<platform>`, `schedule_time_missing`, `rate_limited:<platform>:@<username>:<remaining>`.
  - `warnings` string[] — Top-level advisory warnings (e.g. `rows_exceed_advisory_limit:500`). Empty when none.
  - `rateLimitedAccounts` object[] — Present only when one or more rows targeted an account currently in cooldown. Lets callers map `rate_limited:*` row errors back to structured metadata without parsing the error strings.
    - `accountId` string
    - `platform` string
    - `username` string
    - `rateLimitedUntil` string, date-time

## Other responses

- `207` — Partial success: some rows were created and some failed. Body is identical in shape to the `200` response. Inspect each entry in `results` (`ok` plus `errors`) to see which rows failed and why.
- `400` — Invalid CSV or validation errors
- `401` — Unauthorized
- `402` — Payment required: the account owner has a failed payment. Not returned on dry-run.
- `404` — Authenticated user not found
- `429` — Rate limit exceeded. Possible causes: API rate limit (requests per minute) or account cooldown (one or more accounts for platforms specified in the CSV are temporarily rate-limited).

---

[API](https://skmtc.net/zernio/apis/zernio-api.md) · [All operations](https://skmtc.net/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/zernio/zernio-api/revisions/f81ca70ea6b9/schema)
