API reference

Abni API Reference

Base URL: https://api.abni.ai/v1
Auth: every request must include x-api-key: <your-api-key>
Content-Type: application/json unless otherwise noted

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} until status is completed or failed. 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.

HTTPcodeWhen
400MISSING_BODYRequest body required but absent
400VALIDATION_ERRORBody failed schema validation; details lists per-field failures
400UNSUPPORTED_BRANDbrand slug isn’t yours
400UNSUPPORTED_HIERARCHYsubBrand doesn’t belong to that brand, or campaign doesn’t belong to that sub-brand
400INVALID_SOURCE_URLsourceUrl for brandify isn’t reachable, isn’t HTTPS, or content isn’t an image
401UNAUTHENTICATEDx-api-key missing
403INVALID_API_KEYKey not recognised, revoked, or status ≠ active
403FORBIDDENAuthorized but the operation isn’t allowed for this client
404NOT_FOUNDimageId, jobId, or brand path doesn’t exist for this client
402INSUFFICIENT_CREDITSBalance can’t cover the operation; top up
409ALREADY_PROCESSINGA pipeline is already in flight for that image; wait
413PAYLOAD_TOO_LARGEUpload exceeds 20MB
415UNSUPPORTED_MEDIA_TYPEBrandify upload isn’t an image
429RATE_LIMITEDFuture — when we add request-rate caps
500INTERNAL_ERRORUnexpected server error; include requestId in your support ticket
502UPSTREAM_ERRORImage-provider error we couldn’t recover from
503SERVICE_BUSYTemporary overload; retry with backoff
504UPSTREAM_TIMEOUTImage 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

FieldTypeDefaultNotes
brandstring (slug)required
subBrandstring (slug)optionalMust belong to brand
campaignstring (slug)optionalMust belong to subBrand
promptstringrequiredMax 4000 chars
aspectRatioenum"1:1"1:1 4:5 9:16 16:9 2:3 5:4
resolutionenum"2K"1K 2K 4K
outputFormatenum"png"png jpg
clientReferenceIdstringoptionalEchoed 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"
  }'
FieldTypeDefault
instructionsstringrequired, max 4000
versionnumberoptional; defaults to currentVersion
clientReferenceIdstringoptional

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"
  }'
FieldTypeDefault
aspectRatioenumrequired
resolutionenumrequired
outputFormatenum"png"
versionnumberoptional; defaults to currentVersion
clientReferenceIdstringoptional

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-eventWhen
image.completedNew image (created or brandified) ready
image.revisedRevision produced a new version
image.resizedResize produced a new version
image.failedPipeline 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 requestId from 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.