REST API reference

The upload.ad REST API (v1) lets you drive your whole workspace from any language that can make HTTPS requests: upload creatives to your Meta and TikTok ad accounts, track upload jobs, manage the library and ad copy variants, run review and approval, launch and manage ads, moderate ad comments, and read performance.

Base URL: https://upload.ad

Paid plans only. The REST API, CLI, SDK, and MCP server are part of every paid plan; the free trial covers the app itself. You can subscribe at any time, including during the trial. A workspace without a plan is refused with 402 subscription_required (-32002 over MCP), and an agent that signs in without one is turned away at the authorization screen.

The reference is organized by resource:

  • Uploads: send files and track the jobs that push them to your ad platforms
  • Creatives: the files in your library
  • Copy variants: ad texts attached to each creative
  • Webhooks: signed events when an upload completes or fails, with endpoint management, a delivery log, and redelivery over v1

The API is also described by an OpenAPI specification.

Conventions

Two surfaces, one catalog. The API has a small set of curated REST resources (account, uploads, creatives, ad copy, webhooks) and a tool catalog covering everything else the product can do. The REST resources are a stable convenience layer and are complete as-is: new capabilities land in the tool catalog (GET /api/v1/tools, POST /api/v1/tools/{name}), where they appear on every surface (REST RPC, MCP, CLI, SDK) at once. When both cover the same data, either is fine; prefer the catalog when scripting an agent, the REST resources when you want fixed shapes and multipart uploads.

Authentication. Create an API key at upload.ad/dashboard/settings/api-keys and send it on every request. The free trial covers the app only. Programmatic access requires a paid plan: keys can only be created on a paid plan, and requests return 402 with the code subscription_required if the plan lapses.

A key is bound to the workspace that was active when it was created. Workspace owners and admins can see and revoke every key bound to their workspace.

Authorization: Bearer ua_live_...

Format. Requests and responses are JSON, except POST /api/v1/uploads, which takes multipart/form-data. Timestamps are ISO 8601 strings in UTC.

Idempotency. Every mutating request accepts an Idempotency-Key header. A retry with the same key within 24 hours replays the stored result of the original call instead of running again, so a network timeout can never double-create upload jobs or ads. See Idempotency below for the full semantics.

Pagination. List endpoints are cursor-paginated: each page carries a nextCursor (null on the last page); pass it back as the cursor query parameter for the next page. Cursors are opaque and stable across inserts.

Stability. v1 is stable. We add endpoints, optional parameters, response fields, and tools without notice; we never remove or rename existing ones, and we never change their meaning. Build clients to ignore fields and tools they do not recognize and they will keep working.

Errors. Every error returns a 4xx or 5xx status with a single JSON shape carrying a stable machine-readable code and a human-readable message:

json
{ "error": { "code": "not_found", "message": "Description of the problem" } }

Match on code, not on message (messages may change). The codes:

CodeStatusMeaning
invalid_request400Malformed request body or parameters
unauthorized401Missing or invalid API key
subscription_required402An active paid plan is required
forbidden403The key's creator lacks the permission for this action
not_found404Resource not found, or not visible to your account
conflict409The resource is not in a state that allows the operation
workspace_changed409The request targeted a workspace that is no longer active
rate_limited429Too many requests; retry after the Retry-After header
two_factor_required403The action needs two-factor authentication
internal_error5xxSomething went wrong on our side

Permissions

A key acts with its creator's permissions in the workspace it is bound to. When the member lacks the permission an action needs, the call returns 403 with the code forbidden. Permission-gated actions include creating ads, activating ads and changing budgets, and deleting.

Keys can additionally be scoped at creation: pick a subset of permissions on the API keys page (or pass scopes when creating a key) and the key is limited to those, capped by the creator's own membership. An unscoped key gets everything its creator can do. Scopes cannot be widened after creation; create a new key instead. The scope identifiers:

ScopeGates
uploadUploading creatives to the library
publishPublishing creatives to ad accounts (upload jobs)
editEditing copy, arranging and annotating the canvas
commentCommenting on creatives
reviewApproving creatives and requesting changes
deleteDeleting creatives and folders
importImporting from Google Drive and Dropbox
shareCreating external review links
ads_viewViewing ads and results
adsCreating and editing ads
launchActivating ads and changing budgets (spends money)
ads_deleteDeleting ads
moderateModerating comments on published ads
rulesManaging automation rules
aiThe AI assistant (spends tokens)

Media downloads (/api/media/...) are authorized by object ownership, not by scope: any valid key for the workspace can download its assets.

Scoping follows the workspace policy: per-folder access scoping applies to the library, creatives, and uploads, so a folder-scoped member sees only what their folders contain. Ads, insights, and audiences are workspace-wide and are governed by the Ads permissions rather than folder scope.

Account

GET/api/v1/me

Returns the authenticated account. Useful as a connectivity and key check.

cURL
curl https://upload.ad/api/v1/me \
  -H "Authorization: Bearer $UPLOADAD_API_KEY"
JavaScript
const res = await fetch('https://upload.ad/api/v1/me', {
	headers: { Authorization: `Bearer ${process.env.UPLOADAD_API_KEY}` }
});
const { account } = await res.json();
Python
import os, requests

res = requests.get(
    "https://upload.ad/api/v1/me",
    headers={"Authorization": f"Bearer {os.environ['UPLOADAD_API_KEY']}"},
)
account = res.json()["account"]
PHP
<?php
$ch = curl_init('https://upload.ad/api/v1/me');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . getenv('UPLOADAD_API_KEY')]);
$account = json_decode(curl_exec($ch), true)['account'];
curl_close($ch);
Go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://upload.ad/api/v1/me", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("UPLOADAD_API_KEY"))
	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(string(body))
}
Ruby
require "net/http"
require "json"

uri = URI("https://upload.ad/api/v1/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV['UPLOADAD_API_KEY']}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
account = JSON.parse(res.body)["account"]
Java
import java.net.URI;
import java.net.http.*;

public class Me {
	public static void main(String[] args) throws Exception {
		HttpRequest req = HttpRequest.newBuilder(URI.create("https://upload.ad/api/v1/me"))
			.header("Authorization", "Bearer " + System.getenv("UPLOADAD_API_KEY"))
			.build();
		HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
		System.out.println(res.body());
	}
}
C#
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization",
    $"Bearer {Environment.GetEnvironmentVariable("UPLOADAD_API_KEY")}");
var body = await client.GetStringAsync("https://upload.ad/api/v1/me");
Console.WriteLine(body);
json
{
	"account": {
		"id": "acc_123",
		"name": "Acme Inc",
		"email": "ads@acme.com",
		"plan": "growth",
		"credits": 240,
		"trialEndsAt": null
	}
}
FieldTypeDescription
idstringYour account id (the key's creator, not the workspace owner)
namestringYour account name
emailstringYour account email
planstring or nullThe workspace's current plan, null if not on a plan
creditsinteger or nullRemaining credits; null for keys created by non-owner members
trialEndsAtstring or nullTrial end date, null when on a plan

Tools

Beyond the resource endpoints above, the full upload.ad capability set is available as a catalog of named tools you can call over REST. This is the same tool set the MCP server exposes, so anything an AI agent can do, a script can do too.

GET/api/v1/tools

Returns the full catalog. Each entry has a name, a description, an inputSchema (JSON Schema for the tool's arguments), a category (coarse grouping such as webhooks or automations), a destructive flag, and a readOnly flag (true for tools that only read data). Entries that declare their result shape also carry an outputSchema (same schema dialect, plus nullable); coverage is incremental. The response carries an ETag; send it back as If-None-Match to revalidate and receive 304 Not Modified when the catalog has not changed, instead of re-downloading every schema.

cURL
curl https://upload.ad/api/v1/tools \
  -H "Authorization: Bearer $UPLOADAD_API_KEY"
JavaScript
const res = await fetch('https://upload.ad/api/v1/tools', {
	headers: { Authorization: `Bearer ${process.env.UPLOADAD_API_KEY}` }
});
const { tools } = await res.json();
json
{
	"tools": [
		{
			"name": "create_ad",
			"description": "Create an ad in an ad set or ad group.",
			"inputSchema": { "type": "object", "properties": { "...": {} } },
			"category": "ads",
			"destructive": false,
			"readOnly": false
		}
	]
}

The destructive flag marks tools that spend money or make irreversible changes (creating or activating ads, changing budgets, deleting). Treat it as a signal to confirm before calling.

POST/api/v1/tools/{name}

Call a tool by name. The JSON request body is the tool's arguments; the response is { "result": ... } with whatever the tool returns.

cURL
curl -X POST https://upload.ad/api/v1/tools/get_campaigns \
  -H "Authorization: Bearer $UPLOADAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "meta" }'
JavaScript
const res = await fetch('https://upload.ad/api/v1/tools/get_campaigns', {
	method: 'POST',
	headers: {
		Authorization: `Bearer ${process.env.UPLOADAD_API_KEY}`,
		'Content-Type': 'application/json'
	},
	body: JSON.stringify({ platform: 'meta' })
});
const { result } = await res.json();
json
{ "result": { "campaigns": [] } }

Arguments are schema-validated (types, enums, array limits, numeric ranges). Invalid input returns 400 with a message that starts Invalid input:. Some fields are required only on one platform (for example, Meta ad sets require target countries while TikTok ad groups use location ids instead); the catalog's per-tool schema and descriptions spell out which.

Tool error codes

Tool failures use the standard error envelope, and the code carries the failure kind so clients can branch without parsing prose. The closed set:

CodeStatusMeaning
invalid_input400Bad or missing arguments (includes unknown tool names)
tool_error400Anything else the message explains
limit_exceeded400A size or count cap was hit
not_connected400Needs a platform or integration connection first
plan_required402Needs a (higher) subscription plan
forbidden403Caller lacks the workspace permission or role
not_found404The referenced object does not exist or is outside your access
conflict409Idempotency key conflict; see Idempotency
upstream_error502The ad platform or provider rejected the request
upstream_busy503The ad platform is rate limiting; retry shortly

The same codes appear in MCP error results (the text content is compact JSON of the form {"error":{"code","message"}}) and on the SDK's UploadAdError.code. New codes may be added over time (the set is additive, like the rest of v1); treat unknown codes as tool_error.

Idempotency

Any call that creates ads, uploads files, deletes, or spends money should be sent with an idempotency key so a network retry never runs the action twice. Every mutating endpoint accepts it as an Idempotency-Key request header; tool calls alternatively take an idempotencyKey argument in the body, which also works from the CLI and MCP:

bash
curl -X POST https://upload.ad/api/v1/tools/create_ad \
  -H "Authorization: Bearer $UPLOADAD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-2026-07-21-summer" \
  -d '{ "adsetId": "...", "creativeId": "..." }'

A retry with the same key within 24 hours replays the stored result instead of re-running. A retry that arrives while the original is still in flight returns an error saying the request is already being processed. Keys are scoped to your workspace, may be up to 200 characters, and a single key cannot be reused for a different tool.