Developer documentation

Brand-Pipe API

One HTTPS call turns a raw clip into a platform-ready video: fitted to 9:16, 1:1 or 16:9, with your logo, loudness normalised to −14 LUFS, optionally with burned-in captions. This page is the complete reference.

Base URLhttps://api.brand-pipe.com

1. Get a key

Check out the free tier (25 credits, no card). After checkout you accept the terms and your key is shown once. Keys start with bp_. Store it in a secret manager or your automation tool's credential store.

2. Process a clip

curl
curl -X POST https://api.brand-pipe.com/v1/process \
  -H "X-API-Key: $BRAND_PIPE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://example.com/raw.mp4",
    "logo_url": "https://example.com/logo.png",
    "format": "vertical"
  }'

3. Download the result

200 OK
{
  "success": true,
  "data": {
    "job_id": "6bbee170c2a54f0b9c1e…",
    "result_url": "https://…/6bbee170….mp4?X-Amz-Signature=…",
    "duration_s": 10.0,
    "had_audio": true,
    "format": "vertical",
    "video_reencoded": true,
    "source_color_warning": null,
    "audio_normalized": true,
    "audio_skip_reason": null
  },
  "error": null
}

result_url is a signed link, valid for one hour. The file itself is deleted after 24 hours, so download it or hand it straight to your upload step.

Authentication

Send your key in the X-API-Key header on every request.

header
X-API-Key: bp_your_key

Do not use Authorization: Bearer. The hosting platform in front of the API reads Bearer tokens as its own identity tokens and rejects the request with 401 before Brand-Pipe sees it.

  • Missing or invalid key401.
  • Key has no credits left402. Buy a top-up with the same email; the credits land on the key you already have.
  • Rate limit: 300 requests per minute per key, status polls included → 429. Back off and retry.
  • Lost key: keys are not stored in plaintext. Email support@brand-pipe.com and you get a new key with your remaining credits.

Response envelope

Every response, including errors and unexpected failures, is JSON with the same three fields. Branch on success, not only on the HTTP status.

FieldTypeMeaning
successbooleantrue when the request did what it was asked to.
dataobject | nullThe payload. On some errors it carries extra context, such as credits_refunded or a status_url.
errorstring | nullA human-readable message that is safe to show or log. null on success.
402 Payment Required
{ "success": false, "data": null, "error": "No credits left on this API key." }

Process a video (synchronous)

POST/v1/processanswers when the video is ready · up to 60 s output

Downloads the source, renders it and answers with the result in the same request, typically within seconds. Use it for short clips. For anything longer, or for clients with short timeouts, use POST /v1/jobs.

Headers

HeaderDescription
X-API-KeyrequiredYour API key.
Content-Typerequiredapplication/json
Idempotency-KeyOptional, 1–255 printable characters. Makes retries safe. See Idempotency.

Request body

FieldTypeDefaultDescription
video_urlrequiredURLPublic http(s) URL of the source video. Anything ffmpeg can read: MP4, MOV, WebM, MKV with H.264, HEVC, VP9, AV1 and more. Max 200 MB.
formatstringverticalvertical 1080×1920 (Shorts, TikTok, Reels) · square 1080×1080 · landscape 1920×1080. The source is fitted inside the frame without cropping.
backgroundstringblackWhat fills the frame where the source does not reach: black bars or a blurred, enlarged copy of the video.
logo_urlURLnullPNG with transparency. Scaled to 15 % of the output width and placed 20 px from the bottom-right corner.
audio_normalizebooleantrueNormalise loudness to −14 LUFS integrated, −1 dBTP true peak. Near-silent sources (below −50 LUFS) are left as they are instead of being amplified into hiss.
start_snumber ≥ 00Where the output window starts in the source, in seconds. Snaps to the nearest keyframe, so the cut is accurate to a second or two.
duration_snumber > 0rest of sourceLength of the output window. The duration limit applies to this window, not to the whole source, so you can cut a clip out of a long recording.
subtitlesstringnullSRT or WebVTT text, UTF-8, max 512 KB. Burned into the picture. +1 credit. See Captions.
subtitles_urlURLnullSame, fetched from a public URL. Send either subtitles or subtitles_url, not both.

All URLs must point to public hosts. Private, loopback, link-local and cloud metadata addresses are rejected with 400, including when a redirect leads there.

Response data

FieldTypeDescription
job_idstringIdentifier of this render. With an Idempotency-Key it can also be read on GET /v1/jobs/{job_id}.
result_urlstringSigned download URL for the MP4 (H.264, AAC, fast start). Valid for 1 hour; the file is kept for 24 hours.
duration_snumberDuration of the output in seconds.
had_audiobooleanWhether the source had an audio stream.
formatstringThe preset that was rendered.
video_reencodedbooleanfalse when the source already matched the preset (same resolution, H.264, 8-bit 4:2:0, no logo, no captions). The picture is then copied losslessly and only the audio is processed.
source_color_warningstring | nullSet when the source's colour tags contradict themselves, for example an SD colour matrix on HD material. Reported only, never corrected.
audio_normalizedbooleanWhether loudness normalisation ran.
audio_skip_reasonstring | nullWhy it did not: not requested, source has no audio stream, or a note that the source measured as silent.

Too big for the synchronous endpoint? Before any work starts, the API estimates the encode time. A source that would not finish in time (roughly: more than ~58 s of 1080p30, ~28 s of 1080p60, ~22 s of 4K60) is refused with 413 and a message that names POST /v1/jobs. The credit is returned right away (credits_refunded: true).

Submit an async job

POST/v1/jobsanswers 202 immediately · up to 10 min output

Takes the same body as /v1/process, plus an optional callback_url. It answers in well under a second and renders in the background. The credit is charged when the job is accepted and refunded if it fails.

Extra fieldTypeDescription
callback_urlURLOptional. When the job finishes, its status is POSTed here. See Callbacks.
curl
curl -X POST https://api.brand-pipe.com/v1/jobs \
  -H "X-API-Key: $BRAND_PIPE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: upload-2026-09-16-001" \
  -d '{
    "video_url": "https://example.com/long-recording.mp4",
    "start_s": 30,
    "duration_s": 240,
    "background": "blur",
    "callback_url": "https://hooks.example.com/brand-pipe"
  }'
202 Accepted
{
  "success": true,
  "data": {
    "job_id": "3f9c1a…",
    "status": "queued",
    "status_url": "https://api.brand-pipe.com/v1/jobs/3f9c1a…"
  },
  "error": null
}

Jobs are never dropped under load. They wait in a queue and start as capacity frees up.

Get job status

GET/v1/jobs/{job_id}free · does not use credits
queuedrunningdoneorfailed

Polling costs nothing, but it counts toward the rate limit. An interval of 5–10 seconds is plenty. Only the key that submitted a job can read it; any other key gets 404. Each poll signs a fresh result_url, so a workflow that comes back hours later still gets a working link, as long as that is within 24 hours.

200 OK · done
{
  "success": true,
  "data": {
    "job_id": "3f9c1a…",
    "status": "done",
    "result_url": "https://…signed…",
    "duration_s": 240.0,
    "had_audio": true,
    "format": "vertical",
    "video_reencoded": true,
    "source_color_warning": null,
    "audio_normalized": true,
    "audio_skip_reason": null,
    "error": null,
    "credits_refunded": false,
    "callback_delivered": true
  },
  "error": null
}

A failed job has result_url: null, the same public message in data.error that the synchronous endpoint would have returned, and credits_refunded: true. The request itself still answers 200 with success: true, because the status was read successfully. Check data.status.

Callbacks

Pass callback_url to POST /v1/jobs and you do not have to poll. When the job reaches done or failed, Brand-Pipe POSTs the job status to that URL: the same object as data in GET /v1/jobs/{id}, without the envelope.

  • Content-Type: application/json, User-Agent: BrandPipe/….
  • Any 2xx counts as delivered. A network error or 5xx is retried once after 2 seconds; a 4xx is final. Redirects are not followed.
  • The URL must be public, and it is checked again at delivery time.
  • The outcome is recorded as callback_delivered on the job, so polling remains a fallback if your endpoint was down.

Callbacks are notifications, not authenticated messages. Anyone who knows your callback URL could post to it. If the result matters, read status_url with your own key before acting on it.

Idempotency & retries

Networks drop responses, and automation tools retry. Send an Idempotency-Key header on either POST endpoint and a repeat of the same request returns the first result instead of rendering and charging again. Keys belong to your API key and are remembered for 24 hours.

If the first request with this key…the repeat gets
finishedThe same result (sync: 200 with the stored data and a freshly signed URL; async: 202 with the job's current status), plus the header Idempotent-Replayed: true. No credit.
is still running (sync)409 with data.status_url. Poll it or retry later. No second render is started.
is queued or running (async)202 with the same job_id and its current status.
failedA fresh attempt. The failed one was refunded, so the net cost is one charge.
was rejected before charging (401, 402, 422…)Nothing was stored; the repeat is a normal new request.
had a different body409: a key names one request, not a whole workflow.

Use a value that is unique per item. In n8n that is {{ $execution.id }}-{{ $itemIndex }}. The execution id alone gives every item of a multi-item run the same key, and only the first item would be rendered.

The guarantee covers retries, which always arrive after the first attempt has ended. Two identical requests sent at the same moment can both run.

Burned-in captions

Send an SRT or WebVTT file you already have, such as the output of your transcription step, and it is rendered into the picture. Brand-Pipe does not transcribe.

request body
{
  "video_url": "https://example.com/raw.mp4",
  "subtitles": "1\n00:00:00,500 --> 00:00:02,800\nThree tips for better Shorts\n"
}
FormatsSRT and WebVTT, inline (subtitles) or by URL (subtitles_url). UTF-8 only.
StyleOne style: white, bold, heavy black outline, centred above the area where TikTok and Shorts draw their own buttons. Scales with the output, so every format looks the same. Not configurable yet.
Markup<i>, <b>, <v Speaker> and inline timestamps are removed; HTML entities are resolved. Captions cannot restyle or reposition themselves.
TimingCue times refer to the source. With start_s/duration_s they are shifted onto the output window, and cues outside it are dropped.
Limits512 KB, 2,000 cues, 500 characters per cue, at most 8 cues on screen at once. A broken cue is skipped; a file with no usable cue is a 422.
Cost+1 credit, so a captioned video costs 2. Captions always re-encode the picture.

n8n

Three ready-made workflows are available on the website: n8n-brand-pipe.json (process and download), n8n-brand-pipe-youtube.json (schedule, generate, process, upload to YouTube) and n8n-brand-pipe-jobs.json (async job for long clips, the callback resumes a Wait node).

  1. Create a Header Auth credential named Brand-Pipe API Key with header name X-API-Key and your key as the value.
  2. In the HTTP Request node, add the header Idempotency-Key = {{ $execution.id }}-{{ $itemIndex }} and enable Retry On Fail.
  3. For long clips, use POST /v1/jobs and follow it with a Wait node set to Resume: On Webhook Call, HTTP method POST. Send {{ $execution.resumeUrl }} as callback_url, and set Limit Wait Time so the execution cannot hang forever. n8n-brand-pipe-jobs.json is exactly that, with a timeout branch that reads the status URL. The resume URL has to be reachable from the internet (n8n Cloud is; a local instance needs a public WEBHOOK_URL), otherwise the API rejects it at submission.

Code examples

Submit an async job and poll until it is finished.

python · httpx
import os, time, httpx

API = "https://api.brand-pipe.com"
HEADERS = {"X-API-Key": os.environ["BRAND_PIPE_KEY"]}

def render(video_url: str, idempotency_key: str) -> str:
    r = httpx.post(f"{API}/v1/jobs", headers={**HEADERS, "Idempotency-Key": idempotency_key},
                   json={"video_url": video_url, "format": "vertical"}, timeout=30)
    body = r.json()
    if not body["success"]:
        raise RuntimeError(f"{r.status_code}: {body['error']}")
    status_url = body["data"]["status_url"]

    while True:
        job = httpx.get(status_url, headers=HEADERS, timeout=30).json()["data"]
        if job["status"] == "done":
            return job["result_url"]
        if job["status"] == "failed":
            raise RuntimeError(job["error"])
        time.sleep(5)

Limits

POST /v1/processPOST /v1/jobs
Output length60 s, and the encode-time estimate (see above)600 s (10 min)
Source file200 MB, public http(s) URLsame
Responsewhen the video is ready202 immediately
Rate limit300 requests/min per keysame, status polls included
Download link1 hour1 hour per signing, re-signed on every poll
Retentionresult 24 hresult, job status and idempotency keys 24 h

Processing runs in Frankfurt (Google Cloud); results are stored on Cloudflare R2 in the EU jurisdiction.

Credits & refunds

  • 1 credit per video, whatever its length. 2 credits with captions.
  • Credits are charged when a request is accepted, and refunded automatically if the download, encode or upload fails afterwards. The error response then carries data.credits_refunded: true; failed jobs show the same field.
  • Requests rejected up front (invalid body, blocked URL, bad key) are never charged. A source too large for the endpoint is charged and refunded at once.
  • One exception: an encode that runs into the processing time limit keeps its credit (credits_refunded: false). The estimate refuses almost every such source before work starts; what remains is a source that is far denser than it declares.
  • Status polls and idempotent replays are free.
  • Credits never expire. Top-ups bought with the same email land on your existing key.

Errors

The error field carries the message shown here. Stack traces and internal details are never returned.

StatusMessage / causeWhat to do
400URL is not allowed (private host or non-http scheme) · malformed Idempotency-KeyUse a public URL; key of 1–255 printable characters.
401Invalid or missing API key.Check the X-API-Key header.
402No credits left on this API key.Top up with the same email.
404Job not found. Also returned for jobs that belong to another key or have expired (kept at least 24 hours).Check the job id and key.
409Idempotency key reused with a different body · first request with this key still in progress (data.status_url)Use a new key per request; or poll the status URL.
413Source file larger than 200 MB · source needs more processing time than the endpoint allowsUse POST /v1/jobs, shorten with start_s/duration_s, or send a lower frame rate.
422Invalid request: <fields> · not a readable video or too long · subtitles could not be readFix the named fields; check the file and duration.
424Could not download the source file.Make sure the URL is reachable without login and returns the file.
429Rate limit exceeded for this API key.Back off and retry, with an Idempotency-Key.
500Video processing failed · processing exceeded the time limitRetry once; contact support if it persists.
503Authentication, result storage or the job queue is temporarily unavailable.Retry after a short wait. Safe with an Idempotency-Key.

A good default retry policy: retry 429, 500 and 503 with exponential backoff and an Idempotency-Key. Do not retry other 4xx errors unchanged.

Questions or something not covered here? support@brand-pipe.com