PHero AI API reference

Tag any image or video with one request.

Send prepared media to the endpoint below and get structured JSON back: a description, confidence-scored tags, safety signals, and — for video — a timestamped timeline.

Endpoint

One focused endpoint.

POST https://test.phero-ai.com/api/phero/v1/tag

POST /api/phero/tag powers the free on-site playground only. Billable integrations always use /api/phero/v1/tag with an API key.

Authentication

Bearer keys from your workspace.

Create keys in the API workspace. Each key is shown once — store it immediately. You can hold up to 10 keys and revoke any of them at any time; revoked keys return 401.

Authorization: Bearer phk_YOUR_KEY

Request

Multipart fields.

POST multipart/form-data. Prepare frames as JPEG, PNG, or WebP: at most 3 MB each, 8 MB of frames per request, 10 MB total.

Form fields

mediaType
Required
image or video.
frame
Required
One image frame per part: exactly 1 for images, 1–3 for video. JPEG, PNG, or WebP; at most 3 MB each and 8 MB total.
frameTimestamps
Video only
JSON array of seconds, one entry per frame part (e.g. [0, 4.5, 9]). Must match the frame count.
durationSeconds
Optional
Total video length in seconds. Billable seconds are the larger of this value and the last frame timestamp, rounded up.

Headers

Authorization
Required
Bearer phk_ followed by your 48-character hex API key, created in the API workspace. Shown once — store it immediately.
Idempotency-Key
Optional
Up to 200 characters. Retrying with the same key within the hold window never charges twice.

Preparing media

Downscale frames to JPEG before sending — up to 1600px on the long edge for images, 1280px for video frames, quality around 0.84. The server normalizes uploads the same way, so matching it client-side keeps requests small without changing results. Media is processed transiently and deleted after the response.

Response

Predictable JSON.

description
Always
One factual sentence describing the media (up to 800 characters).
tags
Always
1–15 objects with a lowercase label (up to 80 characters) and a confidence score from 0 to 1.
safety
Always
Content-risk signals from 0 to 1: adult and violence. Built for routing and human moderation support.
timeline
Video only
Up to 8 segments with numeric startSeconds, endSeconds, and a labels array.
unitsCharged
Always
Credits consumed by this request: 1 per image, 1 per analyzed video-second rounded up.
balance
Always
Remaining credit balance after this request.

Example

{
  "description": "A cyclist riding beside a lake at sunrise.",
  "tags": [
    { "label": "cyclist", "confidence": 0.98 },
    { "label": "lake", "confidence": 0.96 },
    { "label": "sunrise", "confidence": 0.91 }
  ],
  "safety": { "adult": 0.01, "violence": 0.02 }
}

Billing

Pay per analyzed unit.

One credit tags one image. Video costs one credit per analyzed second, rounded up, using the larger of the declared duration and the last frame timestamp (minimum 1, capped at one hour). Every response reports unitsCharged and your remaining balance; a short balance returns 402 with both numbers so you can top up the exact gap. See pricing for plans and packs.

Errors

Status codes.

Failures return JSON with an error message and never cache. Retry 502s and 504s with the same idempotency key.

400
Invalid upload: bad media type, frame count, or frame timestamps.
401
Missing, malformed, or revoked API key.
402
Insufficient credits. The response includes your balance and the required units.
413
Prepared media too large: over 10 MB per request, 3 MB per frame, or 8 MB of frames.
415
Frames must be JPEG, PNG, or WebP images that decode cleanly.
429
Rate limited: 60 requests per minute per API key.
502
Tagging failed upstream. Safe to retry (reuse your idempotency key).
503
Tagging or usage tracking is temporarily unavailable.
504
Tagging took too long. Please try again.

Examples

Copy, paste, tag.

Replace phk_YOUR_KEY with a workspace key. Video runs allow up to 3 minutes — set client timeouts accordingly.

curl

Image

curl https://test.phero-ai.com/api/phero/v1/tag \
  -H "Authorization: Bearer phk_YOUR_KEY" \
  -F "mediaType=image" \
  -F "[email protected]"

Video

curl https://test.phero-ai.com/api/phero/v1/tag \
  -H "Authorization: Bearer phk_YOUR_KEY" \
  -H "Idempotency-Key: clip-001" \
  -F "mediaType=video" \
  -F "[email protected]" -F "[email protected]" -F "[email protected]" \
  -F "frameTimestamps=[0, 5, 10]" \
  -F "durationSeconds=12"

Python

Image

import requests

response = requests.post(
    "https://test.phero-ai.com/api/phero/v1/tag",
    headers={"Authorization": "Bearer phk_YOUR_KEY"},
    files={"frame": open("photo.jpg", "rb")},
    data={"mediaType": "image"},
    timeout=120,
)
response.raise_for_status()
print(response.json()["description"])

Video

import requests

frames = [("frame", open(f"frame-{t}.jpg", "rb")) for t in (0, 5, 10)]
response = requests.post(
    "https://test.phero-ai.com/api/phero/v1/tag",
    headers={"Authorization": "Bearer phk_YOUR_KEY", "Idempotency-Key": "clip-001"},
    files=frames,
    data={"mediaType": "video", "frameTimestamps": "[0, 5, 10]", "durationSeconds": "12"},
    timeout=180,
)
response.raise_for_status()
body = response.json()
print(body["timeline"], body["unitsCharged"], body["balance"])

JavaScript

Image

const form = new FormData();
form.append("mediaType", "image");
form.append("frame", photoBlob, "photo.jpg");

const response = await fetch("https://test.phero-ai.com/api/phero/v1/tag", {
  method: "POST",
  headers: { Authorization: "Bearer phk_YOUR_KEY" },
  body: form,
});
if (!response.ok) throw new Error(await response.text());
const { description } = await response.json();

Video

const form = new FormData();
form.append("mediaType", "video");
for (const [blob, name] of frameBlobs) form.append("frame", blob, name);
form.append("frameTimestamps", JSON.stringify([0, 5, 10]));
form.append("durationSeconds", "12");

const response = await fetch("https://test.phero-ai.com/api/phero/v1/tag", {
  method: "POST",
  headers: { Authorization: "Bearer phk_YOUR_KEY", "Idempotency-Key": "clip-001" },
  body: form,
});
if (!response.ok) throw new Error(await response.text());
const { timeline, unitsCharged, balance } = await response.json();

Try it before you integrate.

Run tagging in the browser playground, then grab a key in the workspace.