API

Bucepha Intelligence API

Put Bucepha's intelligence directly into your application.

Programmatically send automotive images to Bucepha and receive structured intelligence through a simple, authenticated API: the damage found and where it is on the photograph, the panels that were visible, how sure the reading is, and what the repair is likely to cost. One photograph at a time, or up to 40 in parallel, each read as its own image.

Keys are made signed in, on your account's API tab. New accounts start with a free month of credits.

5 credits
One photograph, read once
0 credits
A photograph that could not be read
40
Photographs in one batch, read in parallel
8
Endpoints. Nothing you do not need

Playground

Every input, and what comes back.

Pick an endpoint and fill in the variables. The request is what you would send to https://mainbucephaai-production.up.railway.app/v1; the response is what you would get, worked out from your request.

Image URL (https)

Request

curl -X POST https://mainbucephaai-production.up.railway.app/v1/images/process \
  -H "Authorization: Bearer $BUCEPHA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "image": {
    "url": "https://example.com/photos/front-left.jpg"
  }
}'

Response · 200

Or 202, with status processing, when a reading takes longer than the wait.

{
  "id": "img_3kT9xQ2vLm8R0pZa1bYc4dEf",
  "object": "image",
  "status": "complete",
  "created_at": "2026-09-24T12:00:00.000Z",
  "completed_at": "2026-09-24T12:00:06.000Z",
  "reference": null,
  "vehicle": null,
  "metadata": null,
  "result": {
    "readable": true,
    "unread_reason": null,
    "viewpoint": "front left three-quarter",
    "confidence": {
      "percent": 82,
      "band": "high"
    },
    "overall_damage": "moderate",
    "damage": [
      {
        "area": "front_bumper",
        "kind": "scratch",
        "severity": "moderate",
        "confidence": 0.86,
        "description": "Scuffing across the lower left corner of the bumper cover.",
        "bounding_box": {
          "x": 0.12,
          "y": 0.61,
          "width": 0.2,
          "height": 0.09
        },
        "point": {
          "x": 0.2,
          "y": 0.65
        }
      }
    ],
    "panels_visible": [
      "front_bumper",
      "hood",
      "driver_front_door",
      "wheels"
    ],
    "estimated_repair": {
      "low": 280,
      "high": 520,
      "currency": "USD"
    }
  },
  "credits_charged": 5,
  "demonstration": true
}

Nothing is sent from this page and no credit is spent. The vehicle, reference, metadata, counts and charge in the response are worked out from your request exactly as the API would; the damage is a sample, and demonstration: true marks it as one, the same flag the API sets when its findings are not a model's.

Reference

The whole API, on one page.

Authentication, every endpoint with its request and response, every field and its rules, the vocabularies the model answers in, how credits are charged and given back, statuses, errors, limits and security. The same reference is on the API tab of your account.

Quickstart
One photograph, several in parallel, and what is left. Then the same in Node and Python.

One photograph

curl -X POST https://mainbucephaai-production.up.railway.app/v1/images/process \
  -H "Authorization: Bearer $BUCEPHA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "image": {
    "url": "https://example.com/photos/front-left.jpg"
  },
  "vehicle": {
    "year": 2019,
    "make": "Honda",
    "model": "Civic"
  },
  "reference": "stock-1042"
}'

Several photographs in parallel, each its own image

curl -X POST https://mainbucephaai-production.up.railway.app/v1/images/batch \
  -H "Authorization: Bearer $BUCEPHA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "images": [
    {
      "url": "https://example.com/photos/front.jpg"
    },
    {
      "url": "https://example.com/photos/driver-side.jpg"
    },
    {
      "url": "https://example.com/photos/rear.jpg"
    }
  ],
  "vehicle": {
    "year": 2018,
    "make": "Ford",
    "model": "F-150",
    "notes": [
      "tailgate replaced"
    ]
  },
  "reference": "stock-77"
}'

Credits left

curl https://mainbucephaai-production.up.railway.app/v1/credits -H "Authorization: Bearer $BUCEPHA_API_KEY"

Node

const response = await fetch('https://mainbucephaai-production.up.railway.app/v1/images/process', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BUCEPHA_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'stock-1042-front-left', // optional: a retry returns the first image, not a second charge
  },
  body: JSON.stringify({
    image: { url: 'https://example.com/photos/front-left.jpg' },
    vehicle: { year: 2019, make: 'Honda', model: 'Civic' },
    reference: 'stock-1042',
  }),
})

let image = await response.json()
if (!response.ok) throw new Error(`${image.error.code}: ${image.error.message} (${image.error.request_id})`)

// 202 means still reading: poll until it is complete.
while (image.status === 'processing') {
  await new Promise((resolve) => setTimeout(resolve, 2000))
  image = await (
    await fetch(`https://mainbucephaai-production.up.railway.app/v1/images/${image.id}`, {
      headers: { Authorization: `Bearer ${process.env.BUCEPHA_API_KEY}` },
    })
  ).json()
}

console.log(image.status, image.credits_charged, 'credits')
for (const defect of image.result?.damage ?? []) {
  console.log(defect.area, defect.kind, defect.severity, defect.bounding_box)
}

Python

import os, time, requests

BASE = "https://mainbucephaai-production.up.railway.app/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['BUCEPHA_API_KEY']}"}

response = requests.post(
    f"{BASE}/images/process",
    headers=HEADERS,
    json={
        "image": {"url": "https://example.com/photos/front-left.jpg"},
        "vehicle": {"year": 2019, "make": "Honda", "model": "Civic"},
        "reference": "stock-1042",
    },
    timeout=60,
)
image = response.json()
if response.status_code >= 400:
    raise SystemExit(f"{image['error']['code']}: {image['error']['message']} ({image['error']['request_id']})")

while image["status"] == "processing":  # 202: still reading
    time.sleep(2)
    image = requests.get(f"{BASE}/images/{image['id']}", headers=HEADERS, timeout=30).json()

print(image["status"], image["credits_charged"], "credits")
for defect in image["result"]["damage"]:
    print(defect["area"], defect["kind"], defect["severity"], defect["bounding_box"])

The machine-readable version of everything on this page is https://mainbucephaai-production.up.railway.app/v1/openapi.json, OpenAPI 3.1, for generating a client or a Postman collection. It needs no key.

Setup
From a key to a first reading, in any language: the key in your environment, and the JSON in and out.
  1. 1. Get a key, signed in. Open the account's API tab, name a key and press Create key. It is shown once; copy it then. One key per environment, so a key for your laptop and another for production, and revoking one never stops the other.
  2. 2. Keep it in your environment, never in code. Two variables, in a .env file your process reads or in the host's settings:
    BUCEPHA_API_KEY=bk_live_...
    BUCEPHA_API_URL=https://mainbucephaai-production.up.railway.app/v1
    Add .env to .gitignore before the first commit. Point BUCEPHA_API_URL at a local API when developing. If a key reaches a repository, a browser bundle or a log, rotate it on the API tab.
  3. 3. Every request carries the key and JSON.
    POST {BUCEPHA_API_URL}/images/process
    Authorization: Bearer {BUCEPHA_API_KEY}
    Content-Type: application/json
    Idempotency-Key: {your unique id for this attempt}   # optional
    
    { ...the JSON below... }
  4. 4. From a website, call the API from your server. The browser talks to a route of your own; that route holds the key in its environment and forwards the JSON. A key in a page's JavaScript is a key everyone has.
    your page  →  your server route (has BUCEPHA_API_KEY)  →  {BUCEPHA_API_URL}/images/process
    your page  ←  the response JSON, as is, or the parts your page shows  ←
  5. 5. Check the balance first, then send. GET /credits says whether a call would be accepted today, so a batch stops before the first 402 rather than after it.

POST /images/process, the request with every field

{
  "image": {
    "url": "https://example.com/photos/front-left.jpg"
  },
  "vehicle": {
    "year": 2019,
    "make": "Honda",
    "model": "Civic",
    "trim": "EX",
    "body_style": "sedan",
    "color": "blue",
    "mileage": 48000,
    "mileage_unit": "mi",
    "vin": "2HGFC2F69KH512345",
    "notes": [
      "rear bumper repainted"
    ]
  },
  "reference": "stock-1042",
  "metadata": {
    "lane": 7,
    "inspector": "rk"
  }
}

The same, sending the bytes instead of a URL

{
  "image": {
    "base64": "/9j/4AAQSkZJRgABAQ...",
    "content_type": "image/jpeg"
  },
  "reference": "stock-1042"
}

POST /images/process, the response (200, or 202 while processing)

{
  "id": "img_3kT9xQ2vLm8R0pZa1bYc4dEf",
  "object": "image",
  "status": "complete",
  "created_at": "2026-09-24T15:04:05.000Z",
  "completed_at": "2026-09-24T15:04:11.000Z",
  "reference": "stock-1042",
  "vehicle": {
    "year": 2019,
    "make": "Honda",
    "model": "Civic"
  },
  "metadata": {
    "lane": 7
  },
  "result": {
    "readable": true,
    "unread_reason": null,
    "viewpoint": "front left three-quarter",
    "confidence": {
      "percent": 82,
      "band": "high"
    },
    "overall_damage": "moderate",
    "damage": [
      {
        "area": "front_bumper",
        "kind": "scratch",
        "severity": "moderate",
        "confidence": 0.86,
        "description": "Scuffing across the lower left corner of the bumper cover.",
        "bounding_box": {
          "x": 0.12,
          "y": 0.61,
          "width": 0.2,
          "height": 0.09
        },
        "point": {
          "x": 0.2,
          "y": 0.65
        }
      }
    ],
    "panels_visible": [
      "front_bumper",
      "hood",
      "driver_front_door",
      "wheels"
    ],
    "estimated_repair": {
      "low": 280,
      "high": 520,
      "currency": "USD"
    }
  },
  "credits_charged": 5,
  "demonstration": false
}

POST /images/batch, the request

{
  "images": [
    {
      "url": "https://example.com/photos/front.jpg"
    },
    {
      "url": "https://example.com/photos/driver-side.jpg"
    },
    {
      "base64": "/9j/4AAQSkZJRgABAQ...",
      "content_type": "image/jpeg"
    }
  ],
  "vehicle": {
    "year": 2018,
    "make": "Ford",
    "model": "F-150",
    "notes": [
      "tailgate replaced"
    ]
  },
  "reference": "stock-77",
  "metadata": {
    "lot": "B-12"
  }
}

POST /images/batch, the response

{
  "object": "batch",
  "data": [
    {
      "index": 0,
      "image": {
        "...": "an image, as above"
      },
      "error": null
    },
    {
      "index": 1,
      "image": {
        "...": "an image, as above"
      },
      "error": null
    },
    {
      "index": 2,
      "image": null,
      "error": {
        "code": "insufficient_credits",
        "message": "Not enough credits to process an image. Top up to continue."
      }
    }
  ],
  "images_sent": 3,
  "images_read": 2,
  "images_unread": 0,
  "images_processing": 0,
  "images_refused": 1,
  "credits_charged": 10
}

Any error

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "The request is not valid. See details for what to correct.",
    "request_id": "req_Zm9vYmFyYmF6cXV4",
    "param": "vehicle.year",
    "details": [
      {
        "field": "vehicle.year",
        "message": "Too small: expected number to be >=1900"
      }
    ]
  }
}
Authentication
One key, sent as a bearer token on every request.

Create a key signed in, on the account's API tab. It starts with bk_live_, is 51 characters, and is shown once: Bucepha keeps only a fingerprint, so a key that is lost is revoked and replaced, never recovered. The tab lists each key by its name, its first characters, when it was made and when it was last used, and can revoke it or rotate it: a rotation issues a new key under the same name and revokes the old in one call. An account holds up to 5 live keys; make one per environment.

Send it as Authorization: Bearer bk_live_…. Every request is made as the account that owns the key, charged to that account's credits, and can see only that account's images. A key is not a sign-in: it cannot open the website, the extension or the app, and cannot create, list or revoke keys. A website session cannot call the API. The two credentials do not stand in for each other in either direction.

Versioning. Every path is under /v1. Fields are added, never removed or renamed, within a version; a change that would break a caller ships as /v2 beside /v1, and /v1 keeps answering. Treat unknown fields in a response as new, not as errors.

Endpoints
Base address https://mainbucephaai-production.up.railway.app/v1. JSON in, JSON out.
POST /images/process5 credits

Reads one photograph. Send image as a public https URL or as base64 bytes with a content type, plus any of the vehicle fields, a reference and metadata. Answers 200 with the reading when it finishes within about 25 seconds, otherwise 202 with the same shape and status: "processing". Send Idempotency-Key to make a retry safe.

Response

{
  "id": "img_3kT9xQ2vLm8R0pZa1bYc4dEf",
  "object": "image",
  "status": "complete",
  "created_at": "2026-09-24T15:04:05.000Z",
  "completed_at": "2026-09-24T15:04:11.000Z",
  "reference": "stock-1042",
  "vehicle": {
    "year": 2019,
    "make": "Honda",
    "model": "Civic"
  },
  "metadata": {
    "lane": 7
  },
  "result": {
    "readable": true,
    "unread_reason": null,
    "viewpoint": "front left three-quarter",
    "confidence": {
      "percent": 82,
      "band": "high"
    },
    "overall_damage": "moderate",
    "damage": [
      {
        "area": "front_bumper",
        "kind": "scratch",
        "severity": "moderate",
        "confidence": 0.86,
        "description": "Scuffing across the lower left corner of the bumper cover.",
        "bounding_box": {
          "x": 0.12,
          "y": 0.61,
          "width": 0.2,
          "height": 0.09
        },
        "point": {
          "x": 0.2,
          "y": 0.65
        }
      }
    ],
    "panels_visible": [
      "front_bumper",
      "hood",
      "driver_front_door",
      "wheels"
    ],
    "estimated_repair": {
      "low": 280,
      "high": 520,
      "currency": "USD"
    }
  },
  "credits_charged": 5,
  "demonstration": false
}
POST /images/batch5 credits a photograph, up to 40

Reads several photographs in parallel, each as its own image with its own id. Send images, an array of the same sources an image takes; vehicle, reference and metadata apply to every one. Every URL is checked before any is fetched: one the server will not fetch refuses the whole batch and nothing is charged. After that each image succeeds or fails on its own, and the batch says how many were read, came back unread, are still processing, or were refused.

Response

{
  "object": "batch",
  "data": [
    {
      "index": 0,
      "image": {
        "...": "an image, as above"
      },
      "error": null
    },
    {
      "index": 1,
      "image": {
        "...": "an image, as above"
      },
      "error": null
    },
    {
      "index": 2,
      "image": null,
      "error": {
        "code": "insufficient_credits",
        "message": "Not enough credits to process an image. Top up to continue."
      }
    }
  ],
  "images_sent": 3,
  "images_read": 2,
  "images_unread": 0,
  "images_processing": 0,
  "images_refused": 1,
  "credits_charged": 10
}
GET /images/{id}

One image, exactly as it was and is. Poll here after a 202.

GET /images

Past images, newest first. limit 1 to 100 (default 20), reference to filter, and cursor set to the previous page's next_cursor to continue. The response is { object: "list", data: [...], has_more, next_cursor }.

GET /credits

The balance and whether a process call would be accepted today (can_process). Check it before a batch rather than after the first 402.

Response

{
  "object": "credits",
  "credits_remaining": 1240,
  "credits_per_image": 5,
  "images_remaining": 248,
  "credits_used_this_period": 260,
  "period_starts_at": "2026-09-10T14:02:11.000Z",
  "period_ends_at": "2026-10-10T14:02:11.000Z",
  "can_process": true
}
GET /usage/current

This period: images processed, credits used and left, and the period's dates.

Response

{
  "object": "usage",
  "period": {
    "starts_at": "2026-09-10T14:02:11.000Z",
    "ends_at": "2026-10-10T14:02:11.000Z"
  },
  "requests": 52,
  "images_processed": 52,
  "credits_used": 260,
  "credits_remaining": 1240,
  "images_remaining": 248
}
GET /status

{ "object": "status", "status": "ok" } when the API is answering. No key.

GET /openapi.json

The OpenAPI 3.1 document for this API. No key.

Request fields
Every field, its type, whether it is required, and its rules. Unknown fields are refused and named, never ignored.
FieldTypeRequiredRules
imageobjectYesOne image source, below. On /images/batch the field is images, an array of 1 to 40 of them.
image.urlstringOne of url or base64A public https URL up to 2048 characters, fetchable without credentials. Plain http, private networks, localhost and redirects are refused; a URL behind a login is refused unless it is a signed URL that needs no header. JPEG, PNG, WebP or AVIF, up to 12 MB.
image.base64stringOne of url or base64The bytes, base64, up to 12 MB decoded. A data:image/...;base64, prefix is accepted. Needs image.content_type.
image.content_typestringWith base64image/jpeg, image/png or image/webp. Checked against the first bytes.
vehicleobjectNoWhat is known about the car. The more the model is told, the better it separates factory trim and wear from damage.
vehicle.yearintegerNo1900 to 2100.
vehicle.makestringNoUp to 60 characters.
vehicle.modelstringNoUp to 80 characters.
vehicle.trimstringNoUp to 80 characters.
vehicle.body_stylestringNosedan, coupe, pickup, suv and so on. Up to 60 characters.
vehicle.colorstringNoThe paint as sold, which helps the model tell a wrap or a respray from the factory finish.
vehicle.mileageintegerNo0 to 2,000,000, with vehicle.mileage_unit mi or km (default mi).
vehicle.vinstringNo17 characters, no I, O or Q. Echoed back; never used to match cars.
vehicle.notesstring[]NoUp to 10 disclosures of 300 characters, as the seller stated them.
referencestringNoYour own id, up to 200 characters. Comes back on every read; lists filter by it.
metadataobjectNoUp to 20 flat keys of strings (500 characters), numbers, booleans or null. Handed back unchanged. Not searched.
What comes back
Numbers and evidence. The method that produced them stays on the server.
FieldMeaning
idimg_ and 24 characters. Ours to mint: anything else in an id position is 404.
statusprocessing, complete or failed. See Statuses and polling.
created_at, completed_atISO 8601, UTC. completed_at is null while processing.
reference, vehicle, metadataAs you sent them, after validation.
resultNull while processing. The fields below.
credits_chargedCredits taken and kept: 5 for a photograph that was read, 0 for one that was not.
demonstrationTrue only on a deployment with no vision model, where findings are sample data. The live service refuses to start that way.
result.*Meaning
readableFalse when the photograph produced no reading. Then unread_reason says why, and nothing was charged.
viewpointWhere the camera was, as read from the photograph: "front left three-quarter", "interior", and so on.
confidenceHow well the photograph could be read: percent 0 to 100 and a band, low, medium or high.
overall_damageThe worst severity in this photograph.
damage[]Each defect: area, kind, severity, confidence 0 to 1, a description, and where it is.
damage[].bounding_boxx, y, width, height, each 0 to 1 of the image, so it scales to any size you draw it at.
damage[].pointThe most representative point of the defect, where a marker belongs. Not always the centre of the box.
panels_visibleEvery panel the model could see, whether or not it found anything on it. A panel listed here with no damage against it was looked at and is clean.
estimated_repairWhole currency units, low to high. Null when there is nothing to repair.

area Where on the car. unknown when the model could not place it.

  • front_bumper
  • rear_bumper
  • hood
  • roof
  • driver_front_door
  • driver_rear_door
  • passenger_front_door
  • passenger_rear_door
  • driver_quarter_panel
  • passenger_quarter_panel
  • wheels
  • glass
  • lights
  • interior
  • engine_bay
  • undercarriage
  • unknown

kind What the defect is.

  • dent
  • scratch
  • paint_mismatch
  • panel_gap
  • rust
  • crack
  • missing_part
  • tire_wear
  • glass_damage
  • interior_wear
  • other

severity How bad, in the same four words everywhere. none is a photograph with nothing found.

  • none
  • minor
  • moderate
  • severe
unread_reasonMeaning
unreadableThe model could not read it: too dark, too blurred, or not a vehicle.
not_a_photographThe URL served something that is not a photograph: a page, a graphic, a video frame.
too_smallToo few pixels to show a defect.
image_unavailableThe server would not or could not fetch it.
insufficient_creditsThe balance ran out before this photograph was reached. Top up and send it again.
processing_stoppedThe run was halted by a safety stop. Nothing after that point was read or charged.
Drawing the damage on the photograph
The API returns where each defect is, not a drawn image, so you draw on your own copy at any size.

Every position is a fraction of the photograph: bounding_box is x, y, width, height and point is x, y, each 0 to 1 of the image width and height, measured from the top left. Multiply by the size you render the photograph at. The point is where a marker belongs, the most representative spot of the defect, which is not always the centre of the box: on a long scratch it is the deepest part. Either can be null when the model could not place the defect.

Canvas

// The photograph drawn at any size; the coordinates are fractions of it.
const scaleX = canvas.width
const scaleY = canvas.height
for (const defect of image.result.damage) {
  const box = defect.bounding_box
  if (box) ctx.strokeRect(box.x * scaleX, box.y * scaleY, box.width * scaleX, box.height * scaleY)
  const point = defect.point ?? (box && { x: box.x + box.width / 2, y: box.y + box.height / 2 })
  if (point) ctx.fillText(defect.kind, point.x * scaleX, point.y * scaleY)
}
Credits and billing
The same credits and the same balance as the extension and the app.
  • A photograph costs 5 credits, charged once, as it is read. A batch costs 5 credits a photograph read.
  • A photograph that produced no reading costs nothing: an unfetchable URL, a file that is not a photograph, an image too dark or too blurred to read.
  • Every request is charged for the photographs it reads, so a request sent twice without an Idempotency-Key is charged twice. With the key, a retry returns the first image and charges nothing.
  • Processing needs at least 5 credits on the account. Below that, the call is refused with insufficient_credits and nothing is read.
  • A batch with more photographs than the balance covers reads what it can and returns the rest as images with unread_reason: insufficient_credits and no charge. Top up and send those again.
  • The allowance renews every 30 days from the day the account started, and what is left of it expires then. Credits you bought or earned never expire, and are spent after the allowance.
  • Every charge appears in Credit history on the account page, and GET /credits reads the same balance the website shows.
Statuses and polling
A process call reads while you wait, and hands you an id if that takes too long.
POST /images/process
      │  the photograph is fetched and read while the call waits, up to about 25s
      ├── done in time  →  200  status: "complete"  (or "failed": nothing could be read, nothing charged)
      └── not yet       →  202  status: "processing", result: null
                               │
                               └── GET /images/{id} every couple of seconds  →  "complete" or "failed"
statusMeaning
processingStill reading. Returned with 202. result is null until it is done. Poll GET /images/{id}; every two seconds is plenty.
completeDone. result is filled in and credits_charged is final. A complete image can still be unread: readable false, unread_reason set, nothing charged.
failedNothing could be read at all. result says what happened; nothing was charged.

There is no queued state: reading starts the moment the request is accepted. One photograph usually finishes inside the wait. A batch of many usually does not, and the honest answer is a 202 rather than a connection held open for a minute, so treat 202 as the normal path for batches. Nothing is lost by polling: the reading continues on our side whether or not you are connected.

Errors
Every error is { "error": { "type", "code", "message", "request_id", "param"?, "details"? } }.

An error

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "The request is not valid. See details for what to correct.",
    "request_id": "req_Zm9vYmFyYmF6cXV4",
    "param": "vehicle.year",
    "details": [
      {
        "field": "vehicle.year",
        "message": "Too small: expected number to be >=1900"
      }
    ]
  }
}
HTTPtypecodeWhat to do
400invalid_request_errorinvalid_requestA field is missing, malformed or unknown; param and details name it.
400invalid_request_errorinvalid_imageThe URL is not public https, or the bytes are not the image type claimed.
400invalid_request_errorimage_upload_unavailableThis deployment cannot store uploads. Send a URL.
401authentication_errorunauthorizedNo key, a malformed key, or a revoked one.
402billing_errorinsufficient_creditsBelow 5 credits. Nothing was read.
402billing_errorplan_requiredThe free month has ended and no plan is active. Nothing was read.
404not_found_errornot_foundNo image with that id on this account.
409conflict_erroridempotency_conflictThis Idempotency-Key was already used with a different body.
413invalid_request_errorimage_too_largeOver 12 MB.
429rate_limit_errorrate_limitedOver the per-key rate. Read the rate-limit headers and retry.
500api_errorinternal_errorOurs. Retry once; if it persists, quote request_id.
503api_errorservice_unavailableBriefly unavailable. Retry in a moment.

type is the category to branch on; code is the exact case; param is the first field at fault when a field is, and details lists every one. request_id is also the X-Request-ID header on every response, success or error. Quote it if you contact us. An error never describes anything on our side of the line: no stack, no table, no model.

Limits and headers
Per key, and generous for an integration; say if you need more.
LimitValue
Photographs in one batch40
Image size12 MB, as bytes or as a fetched URL
Image URL2048 characters, https, public, no redirects
Image formatsJPEG, PNG, WebP. AVIF by URL.
Request body17 MB
POST /images/process60 a minute
POST /images/batch12 a minute
Reads (GET)600 a minute
Metadata20 keys, values up to 500 characters
Idempotency-Key1 to 128 printable characters
Live keys per account5
Wait on a process callabout 25 seconds, then 202

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets), and a request over the limit is 429 with rate_limited. Back off until the reset rather than retrying at once.

Security
What we do, and what we ask of you.
  • Keys are stored as a SHA-256 fingerprint. Nobody at Bucepha can read your key back.
  • Every URL you send is checked before it is fetched. Private networks, localhost, non-https addresses and redirects are refused, so the API cannot be used to reach anything behind our firewall or yours.
  • Uploaded bytes are checked against their claimed type by their first bytes, stored under a key only your account can sign, and never served to anyone else.
  • The API is a separate boundary from the Bucepha application. It reaches the same reading pipeline through one controlled interface and nothing else: no session, no internal route, no database.
  • Keep the key on your server. Never ship it in a browser, a mobile app or a public repository.
  • Revoke or rotate a key the moment it may have leaked; the old one stops on its next request.
Webhooks
Not yet, and here is why.

A process call reads the photograph while you wait and answers with the result, or with an id to poll a few seconds later; nothing takes long enough for a callback to beat a poll. Webhooks (image.completed, image.failed) arrive when processing becomes asynchronous enough to need them, and they will be listed here and in the OpenAPI document first.