Abni API Reference
This is the public reference for clients integrating against Abni’s image-generation API. Internal-only endpoints and Slack-specific routes aren’t covered here.
Conventions #
Auth #
Every request needs x-api-key. The key is bearer-equivalent — it must live server-side. Don’t expose it in browser code, mobile apps, or public repositories. Rotate by contacting Abni.
Hierarchy #
Brand → Sub-brand → Campaign. Most-specific level wins for context resolution (style, world, golden refs, brand rules). All endpoints accept optional subBrand and campaign slugs alongside brand. Omitting them targets the parent level.
Async results #
Image generation returns 202 Accepted immediately with { id, jobId, status: "processing" }. Two ways to receive the final result:
- Polling:
GET /v1/images/{id}untilstatusiscompletedorfailed. Recommended cadence: 3–5s for the first 60s, back off to 10s. Most images deliver in 20–45s. - Webhooks (recommended for chat bots and async UIs): we POST to a URL you configure at provisioning time when the result is ready. See Webhooks.
Both delivery modes are always active when configured — you can poll AND receive webhooks.
clientReferenceId round-trip #
Every endpoint that creates work accepts an optional clientReferenceId: string (max 200 chars). We echo it back on GET /v1/images/{id} and in webhook payloads, so you can correlate to your own database without a second lookup.
Errors #
Every error response — across every endpoint — uses this shape:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your client balance (4 credits) is below the required 12 for this operation.",
"details": { "balance": 4, "required": 12 },
"requestId": "abni-req-xyz123"
}
}
Branch on code for behaviour. Include requestId in support tickets — we trace it to the exact CloudWatch entry in seconds.
| HTTP | code | When |
|---|---|---|
| 400 | MISSING_BODY | Request body required but absent |
| 400 | VALIDATION_ERROR | Body failed schema validation; details lists per-field failures |
| 400 | UNSUPPORTED_BRAND | brand slug isn’t yours |
| 400 | UNSUPPORTED_HIERARCHY | subBrand doesn’t belong to that brand, or campaign doesn’t belong to that sub-brand |
| 400 | INVALID_SOURCE_URL | sourceUrl for brandify isn’t reachable, isn’t HTTPS, or content isn’t an image |
| 401 | UNAUTHENTICATED | x-api-key missing |
| 403 | INVALID_API_KEY | Key not recognised, revoked, or status ≠ active |
| 403 | FORBIDDEN | Authorized but the operation isn’t allowed for this client |
| 404 | NOT_FOUND | imageId, jobId, or brand path doesn’t exist for this client |
| 402 | INSUFFICIENT_CREDITS | Balance can’t cover the operation; top up |
| 409 | ALREADY_PROCESSING | A pipeline is already in flight for that image; wait |
| 413 | PAYLOAD_TOO_LARGE | Upload exceeds 20MB |
| 415 | UNSUPPORTED_MEDIA_TYPE | Brandify upload isn’t an image |
| 429 | RATE_LIMITED | Future — when we add request-rate caps |
| 500 | INTERNAL_ERROR | Unexpected server error; include requestId in your support ticket |
| 502 | UPSTREAM_ERROR | Image-provider error we couldn’t recover from |
| 503 | SERVICE_BUSY | Temporary overload; retry with backoff |
| 504 | UPSTREAM_TIMEOUT | Image provider timed out |
Endpoints #
#
GET/v1/brands/{brandSlug}/sub-brands?withHero=true
Single round-trip that returns the brand’s sub-brands and campaigns plus their hero images. Designed to feed a UI selector.
curl https://api.abni.ai/v1/brands/cor2ed/sub-brands?withHero=true \
-H "x-api-key: $ABNI_KEY"
Response (200)
{
"brand": { "slug": "cor2ed", "name": "Cor2ED" },
"subBrands": [
{
"slug": "expert-how-to-video",
"name": "Expert How To Video",
"hero": {
"url": "https://cdn.abni.ai/...",
"width": 2048,
"height": 2048,
"imageId": "lib_..."
},
"campaigns": [
{
"slug": "accepted-style",
"name": "Accepted Style",
"hero": { "url": "...", "width": ..., "height": ..., "imageId": "..." }
}
]
}
]
}
hero bubbles up: a campaign without its own hero inherits its sub-brand’s; a sub-brand without one inherits the brand’s. Returns null only when no level in the chain has a hero tagged. Omit ?withHero=true to skip the lookups (faster, but hero will not be populated).
Errors: 404 NOT_FOUND if the brand isn’t yours.
#
POST/v1/images — generate
curl https://api.abni.ai/v1/images \
-X POST \
-H "x-api-key: $ABNI_KEY" \
-H "content-type: application/json" \
-d '{
"brand": "cor2ed",
"subBrand": "expert-how-to-video",
"campaign": "accepted-style",
"prompt": "a doctor holding a brochure on a clinical background",
"aspectRatio": "1:1",
"resolution": "2K",
"outputFormat": "png",
"clientReferenceId": "your-internal-id-123"
}'
Field reference
| Field | Type | Default | Notes |
|---|---|---|---|
brand | string (slug) | required | |
subBrand | string (slug) | optional | Must belong to brand |
campaign | string (slug) | optional | Must belong to subBrand |
prompt | string | required | Max 4000 chars |
aspectRatio | enum | "1:1" | 1:1 4:5 9:16 16:9 2:3 5:4 |
resolution | enum | "2K" | 1K 2K 4K |
outputFormat | enum | "png" | png jpg |
clientReferenceId | string | optional | Echoed back in GET and webhook payloads |
Response (202)
{ "id": "img_a1b2c3d4...", "jobId": "job_...", "status": "processing" }
Then poll GET /v1/images/{id} or wait for the webhook.
Errors: UNSUPPORTED_HIERARCHY, INSUFFICIENT_CREDITS, VALIDATION_ERROR.
#
GET/v1/images/{imageId}
curl https://api.abni.ai/v1/images/img_a1b2c3d4 \
-H "x-api-key: $ABNI_KEY"
Response (200)
{
"id": "img_a1b2c3d4...",
"status": "completed",
"url": "https://cdn.abni.ai/...",
"currentVersion": 1,
"brand": "cor2ed",
"subBrand": "expert-how-to-video",
"campaign": "accepted-style",
"clientReferenceId": "your-internal-id-123",
"versions": [
{
"version": 1,
"url": "https://cdn.abni.ai/...",
"createdAt": "2026-05-05T10:30:00.000Z",
"prompt": "a doctor holding a brochure on a clinical background",
"aspectRatio": "1:1",
"resolution": "2K",
"outputFormat": "png",
"score": 84
}
],
"error": null
}
status is processing (poll again), completed (you have a URL), or failed (error populated). The image URL is a CloudFront link, signed and stable.
#
POST/v1/images/{imageId}/revise — edit
Asks for a content-level change to an existing image. Produces a new version of the same imageId.
curl https://api.abni.ai/v1/images/img_a1b2c3d4/revise \
-X POST \
-H "x-api-key: $ABNI_KEY" \
-H "content-type: application/json" \
-d '{
"instructions": "make the brochure red instead of blue",
"version": 1,
"clientReferenceId": "your-internal-id-123"
}'
| Field | Type | Default |
|---|---|---|
instructions | string | required, max 4000 |
version | number | optional; defaults to currentVersion |
clientReferenceId | string | optional |
Response: 202 with the same shape as POST /v1/images. The new version writes as version: N+1 of the same image. Poll GET /v1/images/{id} to see it appear in versions.
#
POST/v1/images/{imageId}/resize
Surgical — preserves the image content, changes only frame and resolution. Brand context, refs, and rules are NOT re-applied.
curl https://api.abni.ai/v1/images/img_a1b2c3d4/resize \
-X POST \
-H "x-api-key: $ABNI_KEY" \
-H "content-type: application/json" \
-d '{
"aspectRatio": "9:16",
"resolution": "2K",
"outputFormat": "png"
}'
| Field | Type | Default |
|---|---|---|
aspectRatio | enum | required |
resolution | enum | required |
outputFormat | enum | "png" |
version | number | optional; defaults to currentVersion |
clientReferenceId | string | optional |
Response: 202. New version of the same imageId.
#
POST/v1/images/brandify
Take an external image and re-style it to match a brand. Two input modes; pick one.
Mode A — JSON with source URL
curl https://api.abni.ai/v1/images/brandify \
-X POST \
-H "x-api-key: $ABNI_KEY" \
-H "content-type: application/json" \
-d '{
"brand": "cor2ed",
"subBrand": "expert-how-to-video",
"sourceUrl": "https://your-cdn.example.com/source.jpg",
"instructions": "make this look like a Cor2ED clinical photo",
"aspectRatio": "1:1",
"resolution": "2K",
"outputFormat": "png",
"clientReferenceId": "your-internal-id-123"
}'
sourceUrl constraints: HTTPS, reachable from AWS eu-north-1, max 20MB, image content-type.
Mode B — multipart upload
curl https://api.abni.ai/v1/images/brandify \
-X POST \
-H "x-api-key: $ABNI_KEY" \
-F "brand=cor2ed" \
-F "subBrand=expert-how-to-video" \
-F "instructions=make this look like a Cor2ED clinical photo" \
-F "aspectRatio=1:1" \
-F "resolution=2K" \
-F "clientReferenceId=your-internal-id-123" \
-F "source=@./photo.jpg;type=image/jpeg"
Form fields mirror the JSON body. The file must be in a source field, max 20MB.
Response (both modes): 202 { id, jobId, status: "processing" }. The original is preserved as version 1; the brandified version writes as version 2 of the same imageId.
Errors: INVALID_SOURCE_URL, PAYLOAD_TOO_LARGE, UNSUPPORTED_MEDIA_TYPE, UNSUPPORTED_HIERARCHY, INSUFFICIENT_CREDITS.
Webhooks #
When the pipeline completes (or fails), we POST to your configured webhookUrl:
POST https://your-bot.example.com/abni-callback
content-type: application/json
x-abni-event: image.completed
x-abni-signature: t=1714928400,v1=<HMAC-SHA256 hex of "t=<ts>|<raw body>" using your secret>
{
"event": "image.completed",
"id": "img_a1b2c3d4...",
"version": 1,
"url": "https://cdn.abni.ai/...",
"createdAt": "2026-05-05T10:30:00.000Z",
"metadata": {
"brand": "cor2ed",
"subBrand": "expert-how-to-video",
"campaign": "accepted-style",
"prompt": "a doctor holding a brochure...",
"clientReferenceId": "your-internal-id-123"
}
}
Verifying the signature #
Compute HMAC-SHA256("t=<ts>|<raw body>", secret) with the shared secret you received at provisioning. Compare to the v1= value using a constant-time compare. Reject if not equal, OR if t is older than 5 minutes (replay protection).
Pseudo-code:
const [tPart, v1Part] = headerValue.split(",");
const ts = parseInt(tPart.split("=")[1], 10);
const sig = v1Part.split("=")[1];
if (Math.abs(Date.now() / 1000 - ts) > 300) reject();
const expected = crypto.createHmac("sha256", secret).update(`t=${ts}|${rawBody}`).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) reject();
Event types #
x-abni-event | When |
|---|---|
image.completed | New image (created or brandified) ready |
image.revised | Revision produced a new version |
image.resized | Resize produced a new version |
image.failed | Pipeline failed permanently |
Retry policy #
At-least-once delivery. Non-2xx responses or timeouts trigger retries via SQS visibility-timeout redrive. After 6 failed receives, the message lands in the dead-letter queue and the result still sits in GET /v1/images/{id} for reconciliation.
Your endpoint should be idempotent — return 2xx for events you’ve already processed (key on id + version).
Rate limits #
Today: enforced by credit balance only. No request-per-second cap. We’ll add explicit rate limiting (with Retry-After headers) if customer volume warrants — you’ll see a 429 RATE_LIMITED if and when that ships.
Support #
- Include
requestIdfrom any error response in your ticket. - Email: team@abni.ai
- Status / changelog: this doc is the source; we’ll publish breaking changes via email to your provisioning contact at least 30 days in advance.