Download OpenAPI specification:
Abyssale is a programmatic creative asset generation API for creating marketing visuals, ad creatives, videos, and print-ready PDFs at scale. Design once in Abyssale, then generate thousands of personalized variations — across every format, channel, and language — via a single REST API call.
This documentation is about the Abyssale Rest API. If you would like to discover other Abyssale features or to have a comprehensive understanding on how everything works, go to the Abyssale developers hub.
Base URL: https://api.abyssale.com
The API is versioned by release date: vYYYY-MM-DD (e.g. v2026-08-17), never
a counter. Every JSON object payload the API returns — success and error alike — carries a
top-level version field naming the version that answered. The changelog
(CHANGELOG.md) lists every version and what changed in it, breaking changes first.
Content-Type: application/json header.| Use case | Recommended approach |
|---|---|
| Personalized images in emails (rendered on open) | Static design → Dynamic image URL |
| Embed a live, customizable image on a website | Static design → Dynamic image URL |
| Single image on demand (synchronous response) | Synchronous generation |
| Bulk campaign creatives across multiple formats | Async generation |
| Animated GIF or video for social/ads | Animated design → Async generation with gif or video options |
| HTML5 banner ads for ad networks | Animated design → Async generation with html5 options |
| Print-ready PDF (single page) | Printer design → Async generation with print options |
| Multi-page print document (brochure, catalog) | Printer multipage design → Multi-page PDF |
Abyssale uses API Key Authentication. Your API key is passed along with every API call, allowing Abyssale to identify your company account and access your data.
401 Unauthorized — missing or invalid API key429 Too Many Requests — a rate limit was exceeded, or credits are insufficient500 Internal Server Error — unexpected server-side failureTwo limits apply to every request, one after the other. Both answer 429, and the id in the
body tells you which one you hit.
| # | Limit | Applies to | Budget | id |
|---|---|---|---|---|
| 1 | Global ceiling | Every endpoint | 10 requests/second | rate_limit_exceeded |
| 2 | Endpoint budget | Endpoints in a limited tier | see below | request_rate_limited |
1. The global ceiling is 10 requests per second across every endpoint, checked first. It is a burst guard: sustain that rate and you will exhaust most endpoint budgets within the minute anyway, so in practice it is the endpoint budget you meet first.
2. Endpoint budgets. Every endpoint belongs to ONE tier, and the tier sets the budget. The table below covers every endpoint in this reference; a handful of internal and deprecated routes are limited too, but they are not part of the published contract. Each tier is counted separately, so writes never eat into your read budget, and each budget is enforced over two windows at once — per minute and per hour — both of which must pass. Generation endpoints are in no tier: nothing below bounds them per minute or per hour, only the global ceiling and your plan's credits.
| Endpoint | Tier | Per minute | Per hour |
|---|---|---|---|
GET /designs |
Read | 120 | 3 000 |
GET /designs/{designId} |
Read | 120 | 3 000 |
GET /designs/{designId}/formats/{formatSpecifier} |
Read | 120 | 3 000 |
POST /designs/{designId}/dynamic-image-url |
Write | 60 | 1 200 |
GET /designs/{designId}/as-import |
Write | 60 | 1 200 |
GET /designs/import/json |
Read | 120 | 3 000 |
POST /designs/import/json |
Write | 60 | 1 200 |
POST /designs/import/json?validate_only=true |
Validation | 120 | 2 400 |
GET /designs/import/json/{importId} |
Polling | 600 | 18 000 |
PUT /designs/import/json/{importId} |
Write | 60 | 1 200 |
GET /design-duplication-requests/{duplicateRequestId} |
Polling | 600 | 18 000 |
GET /banners/{bannerId} |
Read | 120 | 3 000 |
POST /async/banners/export |
Write | 60 | 1 200 |
GET /fonts |
Read | 120 | 3 000 |
GET /projects |
Read | 120 | 3 000 |
POST /projects |
Write | 60 | 1 200 |
GET /workspace-templates |
Read | 120 | 3 000 |
GET /workspace-template-categories |
Read | 120 | 3 000 |
POST /workspace-templates/{companyTemplateId}/use |
Write | 60 | 1 200 |
GET /generation-request/{generationRequestId} |
Polling | 600 | 18 000 |
POST /auth |
Read | 120 | 3 000 |
POST /banner-builder/{designId}/generate |
none | — | — |
POST /async/banner-builder/{designId}/generate |
none | — | — |
POST /async/banner-builder/{designId}/generate-multipage-pdf |
none | — | — |
Write is the tightest because one call is several database writes, two or more storage
uploads, a queue publish and one outbound fetch per distinct layer src. Count round trips,
not calls: exporting a design and importing it back spends two write calls, so 1 200/hour is
600 export–edit–import cycles.
Validation is looser than write so that iterating on a payload until it validates never costs you an import. It is not free — it still decodes every data URI you send.
Polling is the loosest because those endpoints are meant to be called in a loop. An
import's status response tells you when to come back (next_check_after_ms, 2 seconds while
processing), which is 30 requests a minute for one job. The budget sits far above any sane
cadence — honour next_check_after_ms and you will not meet it.
Generation has no endpoint budget — which is not the same as unlimited. The global 10
requests/second ceiling still applies, and your plan's credits gate it on top
(id: rate_limit_exceeded for both; on a credit refusal retrying never helps, the plan or the
balance has to change). Polling the resulting job IS budgeted, in the polling tier.
Every response from a limited route carries X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset for whichever window you are closest to exhausting, and a 429 adds
Retry-After. Two exceptions, both because the limiter never ran: a 401 (authentication
fails first, so there is no workspace to charge) and the rare case where the limiter's backing
store is unreachable and it lets the request through. Read those headers and pace your calls — this is a limit you can see
coming, which matters most for automated clients and AI agents retrying a rejected payload.
One thing worth knowing before you build a retry loop: a refused request still counts, so
retrying into a 429 spends your hourly budget.
Some fields in this reference are marked deprecated. That mark is a commitment and a warning, in that order:
template_id duplicates id;
category_id / category_name duplicate the project fields on a design).Deprecation is announced in three places, and only these count: the deprecated flag in
this reference, the field's own description, and a dated entry in the
changelog.
A route that disappears from this reference is deprecated too. Omission is how a route is retired here: it keeps answering for existing callers, but it is no longer part of the published contract and should not be adopted. If you are calling something you cannot find documented, treat that as a signal, not as an oversight.
Two things this does not cover. Anything marked Alpha — today, the whole Design Import section — may change or vanish without notice and without a changelog entry; that is what the label means. And a new field appearing in a response is not a breaking change, so parse permissively and ignore what you do not recognise.
Asynchronous work — batch generation, exports — finishes on our side, not in your request. You learn about it in one of two ways: poll the job's status endpoint, or receive a webhook. The payloads are documented at the bottom of this reference; what follows is the delivery contract they all share.
| Property | Detail |
|---|---|
| Method | POST over HTTPS |
| Content type | application/json |
| Acknowledgement | Return 200 or 201 within 15 seconds |
| Retries | The first attempt, then up to 6 retries — about 3 hours in total |
| Backoff | 15 s, 15 s, 3 min, 10 min, 30 min, 2 h |
| Identifying header | X-Referer: api.abyssale.com |
| Static IPs | Available for allow-listing — contact support |
Anything other than 200/201 is treated as a failure and retried; 15 seconds is a hard
timeout, so acknowledge first and do your work afterwards. Answer 410 Gone to stop the
retries permanently — that is how a receiver says the endpoint is retired, and it is the
one response that is never retried.
Every payload carries a top-level event_type (NEW_BANNER, NEW_BANNER_BATCH,
NEW_EXPORT, …). Dispatch on that field rather than inferring the event from the payload's
shape, so a new event type never lands in the wrong branch of your handler.
Two ways to subscribe, and they are not interchangeable. A callback_url in a request
body is per-job: it fires once, for that job only. A webhook configured in the Abyssale
dashboard is per-workspace: it fires for every matching event, including work started outside
the API. NEW_BANNER has no callback_url of its own — it comes from a dashboard webhook.
Design your receiver to be idempotent and to tolerate re-delivery. A retry sends the same payload again, and a webhook is not ordered with respect to your own polling: you may see the job finish through the status endpoint first. Treat the payload as a notification that something is ready, keyed on its id, not as a one-shot event you must not miss.
Requests are not signed. Anyone who learns your callback_url can post to it, so do not
treat the payload alone as proof. Use an unguessable callback path, verify against the API
(GET /generation-request/{id}, GET /banners/{id}) before acting on anything that matters,
and treat X-Referer as a hint rather than authentication.
A design is a layout including at least:
All elements have their own properties that can be customized (color, text payload, image...).
Once designed, all properties of the design will act as a reference for subsequent image generation. Obviously, those properties can be overridden by sending new parameters in the generation API call.
Abyssale provides a list of free template designs in the Template Library page. Feel free to use one to start designing your design instead of doing it from scratch.
All of your designs are listed in the different project pages, only those are accessible via API.
A design moved through the approval workflow — submitted for review, approved, rejected, put on hold, or reset. Use it to tell an internal tool that a creative is cleared for use.
Configured in the dashboard, and filterable. Like newBanner, this event has no
callback_url on any endpoint: you subscribe once for the workspace. The subscription can
be narrowed to specific designs, in which case changes to any other design are not
delivered.
The payload is the design plus the status that triggered the event. That status is
frozen at the moment of the change, not read at delivery time — if a design is
approved and then rejected, you receive two events carrying APPROVED and REJECTED
respectively, rather than two copies of the latest value.
Delivery, retries and the event_type convention are described under
Webhooks.
| event_type required | string Value: "TEMPLATE_STATUS" Identifies the event — every webhook payload carries this field. |
| id required | string <uuid> Unique identifier (UUID) of the design whose status changed. |
| name | string Name of the design. |
| created_at | integer Unix timestamp of the design's creation. |
| updated_at | integer Unix timestamp of the last change to the design's content. |
| status required | string Enum: "NONE" "IN_REVIEW" "ON_HOLD" "REJECTED" "APPROVED" The workflow status this event reports. |
| status_updated_at required | integer Unix timestamp at which that status was set. |
{- "event_type": "TEMPLATE_STATUS",
- "id": "873608a1-e498-47dd-a36d-bd065e3e2b8e",
- "name": "Ad campaign fall 2025",
- "created_at": 1623229458,
- "updated_at": 1649837900,
- "status": "APPROVED",
- "status_updated_at": 1649837939
}Retrieve all designs available in your Abyssale workspace. Each design defines the visual layout, available formats (dimensions), and configurable elements (text, images, shapes, etc.) that can be overridden during image generation. Filter by design type or project using query parameters.
A design lives in a project. Organisation-level master designs are workspace
templates, live in a category, and are listed by GET /workspace-templates
instead — they never appear here.
| project_id | string <uuid> Unique identifier (UUID) of a project. Filter designs by project. |
| type | string Enum: "static" "animated" "printer" "printer_multipage" Filter designs by one of these types: static, animated, printer, printer_multipage. An unknown value is ignored and the full unfiltered list is returned — this never answers 400 (long-standing behavior existing integrations rely on). |
curl -H "x-api-key: $ABYSSALE_API_KEY" \ "https://api.abyssale.com/designs?type=static"
[- {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "template_id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "type": "static",
- "created_at": 1649942114,
- "updated_at": 1649942114,
- "project_id": "9d1f2b7c-5a44-4c3e-9f21-0b8e6d4a1c73",
- "project_name": "Fall campaigns",
- "category_id": "9d1f2b7c-5a44-4c3e-9f21-0b8e6d4a1c73",
- "category_name": "Fall campaigns",
- "version": "string",
}
]Retrieve the full specification of a design: its formats (dimensions and preview URLs), all configurable elements with their properties, and any template variables defined in text layers. Use this endpoint to discover what data to send in a generation request.
For printer designs, each format additionally carries its print settings read-only:
dpi (integer render DPI computed at import time, capped at 300), and bleed_size /
safe_size (floats in the design's unit). All three are always present on a printer
format — a design with no stored print settings, or with a zone disabled, reports the
values the renderer itself uses: dpi: 300 and bleed_size / safe_size of 0, where
0 means the zone is off. Read them unconditionally; there is no need to test for the keys.
For animated designs, the response additionally carries the animation model: a
top-level animation object (duration — timeline length in seconds; screenshot_at_s
— the HTML5 backup-screenshot moment, null on designs created before it was recorded), a per-element
animation object (start_at_s, end_at_s, tweens [{id, type, keyframes}] with
raw keyframe objects) on elements that carry timing or tweens, and video/audio media
attributes (video_url/audio_url, video_duration, video_muted/audio_muted,
max_volume) in the standard attribute shape.
Multipage print designs (printer_multipage) return a different shape. A multipage
design has no formats — each page is one format row — so the response carries pages and
elements_per_page instead of formats, elements and variables. A client coded
against formats[] will not work on a multipage design, and because a dynamic image is
minted per format, a multipage design has no dynamic_image_url anywhere in the response
(it is a static-only feature in any case).
pages[] items are DesignPage objects, not formats: id (page_1 … page_N), width,
height, unit and preview_url — no format uid. elements_per_page is an object
keyed by page id (not an array), each value being that page's element list.
The print settings are returned once, at the root — dpi, bleed_size and
safe_size describe the document, not a page, because a multipage design has a single
print setup. That is the same reason the import declares them at the top level. A page
does not repeat them.
Group layers require ?i=advanced. By default this endpoint returns its original
released shape, in which no group layer appears at all — its children are listed
individually and the group itself is invisible. Pass i=advanced to have them injected
(into elements, or into each page's list in elements_per_page on a multipage design).
Each one carries layer_ids (one list for the whole design — a group holds the same
children in every format), a per-format layout, its computed hidden / locked, and,
except on animated designs, a per-format group block of auto-layout settings
(auto_layout / direction / placement / gap).
A masked group — one whose members are clipped to a shape — is still type: "group"
and additionally carries mask, keyed by format. The geometry can differ per format, but
a group is masked in all of its formats or in none, so the presence of mask is what
distinguishes a masked group from a plain one.
The per-format read GET /designs/{designId}/formats/{formatSpecifier} is always the
advanced view and needs no parameter; there layout, mask and group are flattened to
the single format.
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
| i | string Value: "advanced" Pass |
{- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "template_id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "type": "static",
- "created_at": 1649942114,
- "updated_at": 1649942114,
- "project_id": "9d1f2b7c-5a44-4c3e-9f21-0b8e6d4a1c73",
- "project_name": "Fall campaigns",
- "category_id": "9d1f2b7c-5a44-4c3e-9f21-0b8e6d4a1c73",
- "category_name": "Fall campaigns",
- "version": "string",
- "formats": [
- {
- "id": "facebook-post",
- "uid": "9b57d65e-eb2c-4a74-a51e-4482917c248a",
- "width": 1200,
- "height": 1200,
- "unit": "px",
- "dpi": 300,
- "bleed_size": 3.5,
- "safe_size": 5
}
], - "elements": [
- {
- "name": "element-name",
- "type": "text",
- "layout": {
- "format-id1": {
- "x": 0,
- "y": 0,
- "width": 3333,
- "height": 666
}, - "format-id2": {
- "x": 0,
- "y": 0,
- "width": 3333,
- "height": 666
}
}, - "layer_ids": [
- "string"
], - "hidden": {
- "format-id1": true,
- "format-id2": true
}, - "locked": {
- "format-id1": true,
- "format-id2": true
}, - "group": {
- "format-id1": {
- "auto_layout": true,
- "direction": "string",
- "placement": "string",
- "gap": 0
}, - "format-id2": {
- "auto_layout": true,
- "direction": "string",
- "placement": "string",
- "gap": 0
}
}, - "mask": {
- "format-id1": {
- "shape": "string",
- "width": 0,
- "height": 0,
- "center_x": 0,
- "center_y": 0,
- "rx": 0,
- "ry": 0,
- "rotation": 0,
- "radius": 0
}, - "format-id2": {
- "shape": "string",
- "width": 0,
- "height": 0,
- "center_x": 0,
- "center_y": 0,
- "rx": 0,
- "ry": 0,
- "rotation": 0,
- "radius": 0
}
}, - "animation": {
- "start_at_s": 0.79,
- "end_at_s": 8,
- "tweens": [
- {
- "id": "tb-text_0-slide_2",
- "type": "slide",
- "keyframes": [
- {
- "attr": "opacity",
- "data": {
- "type": "start",
- "time": 0
}
}
]
}
]
}, - "attributes": [
- {
- "id": "payload",
- "help": "Text content (i.e. Lorem ipsum)",
- "values": {
- "facebook-post": "My image title"
}
}
]
}
], - "variables": {
- "name": "{name}",
- "title": "{title}"
}, - "animation": {
- "duration": 8,
- "screenshot_at_s": 8
}, - "dpi": 300,
- "bleed_size": 3,
- "safe_size": 5,
- "pages": [
], - "elements_per_page": {
- "page-id1": [
- {
- "name": "headline",
- "type": "text",
- "settings": {
- "is_mandatory": false
}, - "attributes": {
- "payload": {
- "id": "payload",
- "help": "Text content",
- "value": "Spring Catalogue"
}, - "font_size": {
- "id": "font_size",
- "help": "Font size in pt",
- "value": 36
}
}, - "layout": {
- "x": 0,
- "y": 0,
- "width": 3333,
- "height": 666
}, - "layer_ids": [
- "string"
], - "hidden": true,
- "locked": true,
- "group": {
- "auto_layout": true,
- "direction": "string",
- "placement": "string",
- "gap": 0
}, - "mask": {
- "shape": "string",
- "width": 0,
- "height": 0,
- "center_x": 0,
- "center_y": 0,
- "rx": 0,
- "ry": 0,
- "rotation": 0,
- "radius": 0
}
}
], - "page-id2": [
- {
- "name": "headline",
- "type": "text",
- "settings": {
- "is_mandatory": false
}, - "attributes": {
- "payload": {
- "id": "payload",
- "help": "Text content",
- "value": "Spring Catalogue"
}, - "font_size": {
- "id": "font_size",
- "help": "Font size in pt",
- "value": 36
}
}, - "layout": {
- "x": 0,
- "y": 0,
- "width": 3333,
- "height": 666
}, - "layer_ids": [
- "string"
], - "hidden": true,
- "locked": true,
- "group": {
- "auto_layout": true,
- "direction": "string",
- "placement": "string",
- "gap": 0
}, - "mask": {
- "shape": "string",
- "width": 0,
- "height": 0,
- "center_x": 0,
- "center_y": 0,
- "rx": 0,
- "ry": 0,
- "rotation": 0,
- "radius": 0
}
}
]
}
}Retrieve detailed information for a specific format within a design, including dimensions, unit, preview URL, dynamic image URL, element layout, and variables. Useful for inspecting a single format before generating or building dynamic image URLs.
For printer designs the response additionally carries the format's print settings
read-only: dpi (integer render DPI computed at import time, capped at 300), and
bleed_size / safe_size (floats in the design's unit, 0 when the zone is off). All
three are always present on a printer format; a design with no stored print settings
reports the renderer's own defaults (dpi: 300, zones 0).
For animated designs the response additionally carries the design's animation
object (duration, screenshot_at_s — seconds), per-element animation
timing/tween data, and video/audio media attributes — the same read surface as
GET /designs/{designId}.
This endpoint is always the advanced view. Unlike GET /designs/{designId}, it
needs no i=advanced parameter: it always returns the full property set and always
injects the format's group layers, flattened to this single format (layout, the
group block, and the computed hidden / locked are plain values, not keyed by
format name). A group that does not exist in the requested format is not emitted.
Multipage print designs (printer_multipage): a multipage design is one document —
it has no formats, and its pages are not addressable through this endpoint. Any
formatSpecifier yields 404 with id: format_not_found. Read the whole document with
GET /designs/{designId} or export it with GET /designs/{designId}/as-import.
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
| formatSpecifier required | string Format name or UID. Prefer the UID — it survives a rename. Not applicable to |
{- "id": "facebook-post",
- "uid": "9b57d65e-eb2c-4a74-a51e-4482917c248a",
- "width": 1200,
- "height": 1200,
- "unit": "px",
- "dpi": 300,
- "bleed_size": 3.5,
- "safe_size": 5,
- "animation": {
- "duration": 8,
- "screenshot_at_s": 8
}, - "design": {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "created_at": 1649942114,
- "updated_at": 1649942114,
- "type": "static",
- "category_name": "string"
}, - "elements": [
- {
- "name": "headline",
- "type": "text",
- "settings": {
- "is_mandatory": false
}, - "attributes": [
- {
- "id": "background_color",
- "help": "6 or 8 digits hexadecimal background color (i.e. #F3F3F3) of the banner",
- "value": "#FFFFFF"
}
], - "layout": {
- "x": 0,
- "y": 0,
- "width": 3333,
- "height": 666
}, - "layer_ids": [
- "string"
], - "hidden": true,
- "locked": true,
- "group": {
- "auto_layout": true,
- "direction": "string",
- "placement": "string",
- "gap": 0
}, - "mask": {
- "shape": "string",
- "width": 0,
- "height": 0,
- "center_x": 0,
- "center_y": 0,
- "rx": 0,
- "ry": 0,
- "rotation": 0,
- "radius": 0
}, - "animation": {
- "start_at_s": 0,
- "end_at_s": 0
}
}
], - "variables": {
- "name": "{name}",
- "title": "{title}"
}, - "version": "v2026-08-17"
}Generate images, animated videos, GIFs, HTML5 banners, and print-ready PDFs programmatically from your Abyssale designs.
The generation workflow is simple:
Use the synchronous endpoint for single-image generation with an immediate response. Use the asynchronous endpoint for bulk banner creation across multiple formats at once — ideal for marketing automation, personalized ad campaigns, and batch asset production at scale.
Synchronously generate a single image from a design. Send your dynamic data (text, images, colors) as element overrides in the request body and receive the generated file URL immediately in the response.
static designs only. An animated, printer or printer_multipage design answers
400 with id: template_not_static — use
asynchronous generation
instead. This is a property of the endpoint, not of your plan: a video or a print PDF
cannot be produced inside a synchronous request, so there is no combination of parameters
that makes this work.
Best for: real-time image generation, single-asset workflows, or when you need
the result inline without polling. The rendering budget is hard-capped at 10
seconds: the call returns the finished file within that window or fails with
500 internal_server_error (typical renders take tens of milliseconds).
For bulk generation across multiple formats, use the asynchronous endpoint instead.
AI features and the 10-second cap. text_to_image is not accepted here — an AI
generation takes far longer than the budget, so the field exists only on the asynchronous
endpoint and sending it answers 400 invalid_payload. Background removal (remove_bg)
is accepted but is deprecated on this endpoint: it adds an AI round-trip before the
render, so a large or slow source image can push the call past the cap and fail with
500 internal_server_error rather than returning your asset. Existing integrations keep
working; new ones should remove backgrounds asynchronously. auto_focus is unaffected
and fully supported here — it runs on Abyssale's own detection model and is fast enough
that it does not put the budget at risk.
All body fields are optional — omit elements to render the design's saved
default content. A template_format_name that does not exist on the design answers
404 with id: format_not_found (the format is addressed as a resource, so this is
a 404 by contract, not a 400).
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
object Element overrides keyed by layer name. On this synchronous endpoint every value MUST be an object — a bare string value answers | |
| template_format_name | string Format ID to generate. Optional — when omitted, the design's first format is generated. |
| image_file_type | string Enum: "png" "jpeg" "webp" "avif" "pdf" "auto" Output file type ( |
| original_visual_id | string <uuid> Regenerate an existing visual in place, keeping its share URL (visual versioning). Unknown or unrelated visuals answer 404 ( |
| file_compression_level | integer [ 1 .. 100 ] Output quality, 1-100 — 100 is the best quality (least compression), 1 the smallest file. Despite the name, higher = better. |
{- "template_format_name": "facebook-post",
- "image_file_type": "png",
- "elements": {
- "root": {
- "background_color": "#F3F3F3"
}, - "title": {
- "payload": "Summer sale — 40% off",
- "color": "#111111"
}, - "cta": {
- "payload": "Shop now",
- "background_color": "#5B4CF5"
}
}
}{- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": 1,
- "sharing_id": "5fcec999-2bfb-4dd7-ba38-2d9e16c49149",
- "file": {
- "type": "png",
- "filename": "996739f4-b563-428a-a6e8-ec3cb8bd03d4.png"
}, - "format": {
- "id": "facebook-post",
- "unit": "px",
- "width": 1200,
- "height": 1200
}
}Asynchronously generate multiple formats of an image, video, GIF, HTML5 banner, or PDF in a single API call. Ideal for bulk banner production, multi-channel marketing campaigns, and programmatic ad creative generation at scale.
Specify one or more format IDs to generate, or omit the list to generate all
formats defined in the design. Batches are queued and worker-driven, with no hard
completion bound — most complete within a couple of minutes; rely on the webhook or
polling, never a fixed wait. Provide a callback_url to receive a webhook
notification when generation is complete, or poll
GET /generation-request/{generationRequestId} for status.
You can find available formats and elements by calling GET /designs/{designId}.
This is where AI image work belongs. Having no completion bound, this endpoint is the
only one that accepts text_to_image (AI generation and inpainting), and it is the
recommended home for remove_bg too — both add an AI round-trip that the synchronous
endpoint's 10-second cap cannot absorb.
All body fields are optional — omit elements to render the design's saved
default content, and omit template_format_names to generate every format. A
template_format_names entry that does not exist on the design answers 404 with
id: format_not_found (the format is addressed as a resource, so this is a 404 by
contract, not a 400).
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
object (AsyncElements) Same as | |
| template_format_names | Array of strings Format IDs you would like to generate. The array can be empty or not provided if you want to generate all formats of this design. |
| callback_url | string <uri> The url that will be called once generation of your images is done. We will do a |
| image_file_type | string Enum: "png" "jpeg" "webp" "avif" "gif" "pdf" "html5" "mp4" "auto" Output file type ( |
| file_compression_level | integer [ 1 .. 100 ] Output quality, 1-100 — 100 is the best quality (least compression), 1 the smallest file. Despite the name, higher = better. |
object HTML5 output options (animated designs only) | |
object GIF output options (animated designs only) | |
object Video output options (animated designs only) | |
object Print output options (printer/printer_multipage designs only) | |
object
| |
| original_visual_id | string <uuid> UUID of the original visual this generation is based on. |
{- "template_format_names": [
- "facebook-feed",
- "instagram-post"
], - "image_file_type": "png",
- "elements": {
- "root": {
- "background_color": "#F3F3F3"
}, - "title": {
- "payload": "Summer sale — 40% off",
- "color": "#111111"
},
}
}{- "generation_request_id": "3f2a9c18-6b1e-4f4a-9a75-2b6d0e1c8f77"
}Asynchronously generate a multi-page print-ready PDF from a printer_multipage design.
Each page is defined by a pages dictionary entry, allowing different element
overrides per page — ideal for brochures, catalogs, and variable data printing.
Provide a callback_url to receive a webhook when the PDF is ready,
or poll GET /generation-request/{generationRequestId} for status.
You can find available formats and elements by calling GET /designs/{designId}.
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
required | object (Pages) Per-page element overrides, keyed by page identifier ( |
| callback_url | string <uri> The url that will be called once generation of your PDF is done. We will do a |
object Print output options | |
| original_visual_id | string <uuid> UUID of the original visual this generation is based on. |
{- "pages": {
- "page-id1": {
- "element-name1": {
- "background_color": "#FF0000"
}, - "element-name2": {
- "background_color": "#FF0000"
}
}, - "page-id2": {
- "element-name1": {
- "background_color": "#FF0000"
}, - "element-name2": {
- "background_color": "#FF0000"
}
}
}, - "print": {
- "color_profile": "b4bc069b-fba6-4b13-aec1-3db2eeb7dbd7",
- "display_crop_marks": true
}, - "original_visual_id": "efce0750-ba21-49c4-be6f-4e6125cdc6bb"
}{- "generation_request_id": "06399fcd-0c21-47da-bd9b-1e653e0453e8",
- "version": "v2026-08-17"
}Poll the status of an asynchronous generation request. Use this endpoint to track the progress of a batch generation operation started with the async generate endpoint. The result is available for 7 days after generation.
| generationRequestId required | string <uuid> Unique identifier (UUID) of the generation request |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "is_finalized": true,
- "banners": [
- {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": 1,
- "sharing_id": "5fcec999-2bfb-4dd7-ba38-2d9e16c49149",
- "file": {
- "type": "jpeg",
- "filename": "996739f4-b563-428a-a6e8-ec3cb8bd03d4.jpeg",
- "fallback_image_url": "string"
}, - "format": {
- "id": "facebook-post",
- "unit": "px",
- "width": 1200,
- "height": 1200
}, - "template": {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "created_at": 1649942114,
- "updated_at": 1649942114
}, - "project": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at_ts": 0
}, - "edit_url": "string",
- "view_url": "string",
- "visual_status": {
- "status": "string",
- "status_updated_at_ts": 0,
- "reason": "string",
- "content_to_replace": "string",
- "content_to_hide": "string"
}
}
], - "errors": [
- {
- "template_format_name": "string",
- "reason": "string"
}
], - "version": "v2026-08-17"
}Every request is authenticated with an x-api-key header carrying a key generated from
your Workspace settings.
Use POST /auth to confirm a key works and to find out which workspace it belongs to.
Any problem with the key answers 401 — unknown, revoked, or on a plan that does not
include API access. The id in the body distinguishes them: api_access_denied is the
plan case, unauthorized covers the rest.
Use a workspace key, not a user key. A user-scoped key exists to confine a credential
to the design plugins: it reaches only POST /auth, GET /fonts and the plugin endpoints,
and every other operation in this reference answers 401, whatever the plan. It is not
plan-checked either — so it answers 200 on /auth even where a workspace key on the same
plan would answer 401 api_access_denied. If a key passes /auth but 401s everywhere
else, that is why: it is a user key, and it was never meant to drive the API.
Check that an API key is usable, and find out which workspace it belongs to.
This is the endpoint to call when you want to confirm a key works — it runs the whole
authentication path and answers 200 only if all of the following hold:
401),401 api_access_denied
otherwise).The plan check is skipped for a user-scoped key, which therefore answers 200 here
whatever the plan. That is not a loophole: a user key is confined to the plugin surface,
so a 200 from this endpoint means "this key is live", not "this key can generate". If
you need to know whether a key can drive the API, call a real endpoint with it.
Every failure is a 401, whether the key is unknown, revoked, or on a plan without
API access. This endpoint does not answer 403.
The response carries the workspace name, so a caller holding several keys can tell which one it has. Integration platforms use it to label a connection.
Do not use
GET /readyto test a key. It is a service health check and is exempt from authentication, so it answers200regardless of the key you send — including one that has been revoked.
Takes no request body. It is a POST for historical reasons; live integrations depend on
the current shape, so it will not be changed.
curl -X POST -H "x-api-key: $ABYSSALE_API_KEY" \ https://api.abyssale.com/auth # {"company":"Acme Inc.","version":"v2026-08-17"}
{- "company": "Acme Inc.",
- "version": "v2026-08-17"
}Retrieve the fonts available in your Abyssale workspace. Fonts can be applied to text and button elements during image generation. Two types are supported: Google Fonts (free, always available) and custom fonts uploaded to your workspace.
Reference a font by its ID when overriding the font property in a generation request.
Retrieve all fonts available in your Abyssale workspace, including Google Fonts
and any custom fonts you have uploaded. Use a font's id to override the font
on text or button elements in a generation request.
| type | string Enum: "google" "custom" Filter fonts by type. Omit to return both Google and custom fonts. An unknown value is ignored and the full unfiltered list is returned — this never answers 400 (long-standing behavior existing integrations rely on). |
[- {
- "id": "61568e7c-33c5-11ea-9877-92672c1b8195",
- "name": "Ubuntu Mono",
- "available_weights": [
- 100
], - "type": "google"
}
]Export a collection of previously generated banners as a single ZIP archive. Useful for downloading bulk assets for offline use, sharing with creative teams, or archiving a campaign's outputs. The export is processed asynchronously — provide a callback URL to receive a webhook notification when the archive is ready.
An asynchronous banner export has completed. The ZIP archive containing all
requested banners is available at the provided archive_url.
| event_type | string Value: "NEW_EXPORT" Identifies the event — every webhook payload carries this field. |
| export_id | string <uuid> ID of the export |
| archive_url | string <uri> The zip public URL |
| requested_at | number Timestamp at which the export has been requested |
| generated_at | number Timestamp at which the export has been done |
{- "event_type": "NEW_EXPORT",
- "export_id": "38cb7df3-1160-4824-8531-2bacde2b6517",
- "requested_at": 1642150000,
- "generated_at": 1642150002
}Asynchronously package a set of previously generated assets — any output type
(JPEG, PNG, WEBP, AVIF, PDF, MP4, GIF, HTML5 bundle) — into a single ZIP
archive for download. Provide a callback_url to receive a webhook notification
when the export is ready, containing the archive URL.
| ids required | Array of strings <uuid> non-empty [ items <uuid > ] An array of string that corresponds to the banners IDs you would like to export. |
| callback_url | string <uri> The url that will be called once the generation of your export is done. |
{- "ids": [
- "64238d01-d402-474b-8c2d-fbc957e9d290"
],
}{- "export_id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": "v2026-08-17"
}Retrieve metadata and download URLs for generated files (images, videos, GIFs, HTML5 banners, and PDFs). Listen to webhook events to be notified in real time when a file or batch generation completes.
A new file (image, video, GIF, HTML5 banner, or PDF) has been generated and is ready to download.
Subscribed in the dashboard, not per request. Unlike the other two events, this one
has no callback_url field on any endpoint — it is configured once for the workspace and
then fires for every file generated, including files produced outside the API.
Delivery, retries and the event_type convention are described under
Webhooks.
| event_type | string Value: "NEW_BANNER" Identifies the event — every webhook payload carries this field. |
| id required | string <uuid> |
| version | integer Version number of the generated file — an integer counter, NOT the API version.
A banner response reports this counter; the |
| sharing_id | string <uuid> Identifier used for sharing this generated file. |
required | object (File) |
object (Format) | |
object The design this file was generated from. | |
object The design's project — present when the design has one. | |
object Deprecated Deprecated — same information as | |
| edit_url | string Platform edit URL — only for visuals bookmarked or downloaded in the platform. |
| view_url | string Platform view URL — only for visuals bookmarked or downloaded in the platform. |
object Present when the visual carries a review status. |
{- "event_type": "NEW_BANNER",
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": 1,
- "sharing_id": "5fcec999-2bfb-4dd7-ba38-2d9e16c49149",
- "file": {
- "type": "jpeg",
- "filename": "996739f4-b563-428a-a6e8-ec3cb8bd03d4.jpeg",
- "fallback_image_url": "string"
}, - "format": {
- "id": "facebook-post",
- "unit": "px",
- "width": 1200,
- "height": 1200
}, - "template": {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "created_at": 1649942114,
- "updated_at": 1649942114
}, - "project": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at_ts": 0
}, - "edit_url": "string",
- "view_url": "string",
- "visual_status": {
- "status": "string",
- "status_updated_at_ts": 0,
- "reason": "string",
- "content_to_replace": "string",
- "content_to_hide": "string"
}
}An asynchronous batch generation request has completed. Contains the full list
of generated files and any per-format errors. Triggered by
POST /async/banner-builder/{designId}/generate.
| event_type | string Value: "NEW_BANNER_BATCH" Identifies the event — every webhook payload carries this field. |
| generation_request_id required | string <uuid> Unique identifier (UUID) of the batch generation |
required | Array of objects (Banner) List of generated images |
Array of objects If any errors occur, they will be listed under that property |
{- "event_type": "NEW_BANNER_BATCH",
- "generation_request_id": "06399fcd-0c21-47da-bd9b-1e653e0453e8",
- "banners": [
- {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": 1,
- "sharing_id": "5fcec999-2bfb-4dd7-ba38-2d9e16c49149",
- "file": {
- "type": "jpeg",
- "filename": "996739f4-b563-428a-a6e8-ec3cb8bd03d4.jpeg",
- "fallback_image_url": "string"
}, - "format": {
- "id": "facebook-post",
- "unit": "px",
- "width": 1200,
- "height": 1200
}, - "template": {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "created_at": 1649942114,
- "updated_at": 1649942114
}, - "project": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at_ts": 0
}, - "edit_url": "string",
- "view_url": "string",
- "visual_status": {
- "status": "string",
- "status_updated_at_ts": 0,
- "reason": "string",
- "content_to_replace": "string",
- "content_to_hide": "string"
}
}
], - "errors": [
- {
- "template_format_name": "string",
- "reason": "string"
}
]
}Retrieve the metadata and download URLs for a previously generated file (image, video, GIF, HTML5 banner, or PDF). Returns both the S3 storage URL and a CDN-hosted URL suitable for embedding directly in websites or emails.
| bannerId required | string <uuid> Unique identifier (UUID) of the file |
{- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "version": 1,
- "sharing_id": "5fcec999-2bfb-4dd7-ba38-2d9e16c49149",
- "file": {
- "type": "jpeg",
- "filename": "996739f4-b563-428a-a6e8-ec3cb8bd03d4.jpeg",
- "fallback_image_url": "string"
}, - "format": {
- "id": "facebook-post",
- "unit": "px",
- "width": 1200,
- "height": 1200
}, - "template": {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Ad campaign fall 2025",
- "created_at": 1649942114,
- "updated_at": 1649942114
}, - "project": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "created_at_ts": 0
}, - "edit_url": "string",
- "view_url": "string",
- "visual_status": {
- "status": "string",
- "status_updated_at_ts": 0,
- "reason": "string",
- "content_to_replace": "string",
- "content_to_hide": "string"
}
}Projects let you organize your designs into logical groups — for example by campaign, client, or content type. All designs must belong to a project to be accessible via the API. Use projects to scope design retrieval and keep large design libraries manageable.
Retrieve all projects in your workspace. Projects group designs into logical collections. Only designs that belong to a project are accessible via the API.
[- {
- "id": "9d00e9fe-9bd5-4471-acad-baa702a867e9",
- "name": "Animation Tests",
- "created_at_ts": 1654763057
}, - {
- "id": "bdf9ab44-f5fd-47ad-881d-a45906901233",
- "name": "HTML5 Tests",
- "created_at_ts": 1663767647
}
]Create a new project to organize your designs. Assign designs to this project in the Abyssale editor to make them accessible via the API.
| name required | string [ 2 .. 100 ] characters Name of the project |
{- "name": "Summer Campaign 2024"
}{- "id": "cb2c4add-4867-11f0-96f2-0a00d9eb8f78",
- "name": "Summer Campaign 2024",
- "created_at_ts": 1749827107
}Workspace templates are master designs shared across your organization. Duplicate them into any project to create editable copies — useful for spinning up campaign variants, onboarding new clients, or maintaining brand-consistent starting points. The duplication is processed asynchronously; poll the status endpoint to track progress.
Retrieve the organisation-level master designs shared across your workspace.
A workspace template lives in a category (category_id), not a project — it
belongs to the organisation until you duplicate it into a project with
POST /workspace-templates/{companyTemplateId}/use. Designs, which do live in a
project, are listed by GET /designs; the two listings never overlap.
Items have the same shape as a design, except the grouping fields are named
category_id / category_name.
| category_id | string <uuid> Unique identifier (UUID) of a workspace category. List the available ids with
|
| type | string Enum: "static" "printer" "animated" "printer_multipage" Filter workspace templates by one of these types: static, animated, printer, printer_multipage. An unknown value is ignored and the full unfiltered list is returned — this never answers 400 (long-standing behavior existing integrations rely on). |
[- {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Brand master",
- "type": "static",
- "created_at": 1649942114,
- "updated_at": 1649942114,
- "category_id": "1c7a9e35-0b62-4d18-8f4a-2e5c7b90d146",
- "category_name": "Brand assets"
}
]The categories that group your workspace templates. Use an id from here as the
category_id filter on GET /workspace-templates.
This is the workspace-template counterpart of GET /projects, which groups designs.
[- {
- "id": "1c7a9e35-0b62-4d18-8f4a-2e5c7b90d146",
- "name": "Brand assets",
- "color": "#4F46E5",
- "icon": "star",
- "created_at": 1649942114
}
]Copy a shared workspace template into one of your projects, creating an editable
design instance. The duplication is asynchronous — use the returned
duplication_request_id to poll for completion via
GET /design-duplication-requests/{duplicateRequestId}.
| companyTemplateId required | string <uuid> Example: 0c967bd0-4137-4690-ad70-249aa021c68b Unique identifier (UUID) of the workspace template to duplicate |
| project_id required | string <uuid> Target project ID where the template will be duplicated |
| name | string [ 2 .. 100 ] characters Optional custom name for the duplicated template |
{- "project_id": "d59adee9-4867-11f0-96f2-0a00d9eb8f78"
}{- "duplication_request_id": "40c32a4e-4869-11f0-96f2-0a00d9eb8f78"
}Poll the status of a template duplication request. Use this endpoint to track the progress of an asynchronous duplication operation.
| duplicateRequestId required | string <uuid> Example: 40c32a4e-4869-11f0-96f2-0a00d9eb8f78 Unique identifier (UUID) of the duplication request |
{- "request_id": "40c32a4e-4869-11f0-96f2-0a00d9eb8f78",
- "status": "COMPLETED",
- "created_at_ts": 1749827734,
- "completed_at_ts": 1749827736,
- "errored_at_ts": null,
- "target_project": {
- "id": "d59adee9-4867-11f0-96f2-0a00d9eb8f78",
- "name": "HTML5 Tests",
- "created_at_ts": 1749827125
}, - "designs": [
- {
- "original_design_id": "0c967bd0-4137-4690-ad70-249aa021c68b",
- "target_design_id": "afb1a61a-6c50-4bc3-a49b-3381822d4e81",
- "target_design_name": "New Design name"
}
]
}Alpha. This API is in Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. It is not covered by the deprecation policy. This applies to every operation in this section.
Create a design from a JSON structure — static, animated, printer or
printer_multipage. The import produces a design in a project by default; pass
target.kind: "workspace_template" for a reusable workspace blueprint instead.
/designs/import/json — submit the structure. Returns an import id and a flat
uploads[] array of presigned S3 POSTs, one per image/logo layer whose src is a
filename.fields./designs/import/json/{importId} — validate and queue the import.No upload needed when every src is a public URL or data URI: the POST returns the
import already QUEUED with an empty uploads[], and there is no step 2 or 3.
Dry run: POST …?validate_only=true runs schema and payload cross-field validation
without creating anything — 204 on success, the error contract on failure. Three checks
need I/O and are skipped, so a 204 does not guarantee the real POST succeeds:
unknown_font, project_not_found and unreachable_src.
Poll GET /designs/import/json/{importId} for status. It carries next_check_after_ms
(stop polling when it is null), structured warnings[], and a structured error when
status is FAILED. A failed import is still HTTP 200 — branch on status, not on the
status code. Thumbnails are rendered for you; the payload has no preview image field
(preview_image is unknown_field).
Errors use the same envelope as the rest of the API — a top-level id and message,
plus an errors array locating each problem. A rejected payload usually fails in several
places at once, which is what errors is for; id is the shared code when every entry
agrees and invalid_payload when they differ:
{
"id": "out_of_range",
"message": "font_size must be between 2 and 1000",
"errors": [
{ "path": "layers[2].properties.font_size", "code": "out_of_range",
"message": "font_size must be between 2 and 1000",
"expected": { "min": 2, "max": 1000 }, "received": 1500 }
]
}
Round trip: GET /designs/{designId}/as-import returns any existing design — every
type, multipage included — in the exact shape this endpoint accepts.
Per design type — full rules in the guides linked above:
| Type | Authored in | Colors | Key limits |
|---|---|---|---|
static |
px, integer | hex | format 1–5000 px |
animated |
px, integer | hex | required animation.duration 1–60 s; format within a 1920×1200 box in either orientation |
printer |
one top-level unit (mm|in), float geometry, font_size in pt 0.48–240 |
cmyk()/cmyka(); a solid hex is converted with a color_converted warning; gradients are rejected |
layer assets 500 MB presigned |
printer_multipage |
as printer, print setup declared once at the top level |
as printer |
pages 1–30, each 1–80 layers |
Data URIs are capped at 2 MB decoded on every type. Presigned layer assets: 20 MB static, 100 MB video / 25 MB audio / 20 MB image on animated, 500 MB on print.
Alpha. This API is in Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. It is not covered by deprecation policy.
List this company's JSON template imports, newest first.
Use this to recover an import whose id you no longer have. Because the flow is
POST → upload → PUT, a client that loses the id between those steps leaves behind an
import it cannot otherwise resume or inspect; listing is how you find it again.
Rows are summaries. The unbounded warnings[] array and the error object are served by
GET /designs/import/json/{importId} — a row carries warning_count instead, and a
links.status pointing at that endpoint.
status filters on the effective status, matching what the item endpoint reports: an
import whose presigned upload window has closed counts as FAILED, not
WAITING_FOR_VALIDATION.
Lists JSON imports exclusively — imports created through other channels are not returned.
No cursor. Results are capped by limit; narrow with created_after /
created_before to reach imports beyond the cap.
| status | string Enum: "WAITING_FOR_VALIDATION" "QUEUED" "PROCESSING" "DONE" "FAILED" Return only imports whose effective status matches. |
| created_after | integer Unix timestamp (seconds). Return imports created at or after this moment. |
| created_before | integer Unix timestamp (seconds). Return imports created at or before this moment. |
| limit | integer [ 1 .. 200 ] Default: 50 Maximum number of imports to return. |
[- {
- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "name": "Autumn card",
- "status": "WAITING_FOR_VALIDATION",
- "created_at_ts": 1649942114,
- "validated_at_ts": 1649942200,
- "warning_count": 3,
- "result": {
- "kind": "workspace_template",
- "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f",
- "project_uuid": "1de37675-f868-4726-bb1d-b08d89f463f9"
}
}
]Alpha. This API is in Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. It is not covered by deprecation policy.
Submit a template structure as JSON. Returns an import ID, a links block, and a flat
uploads[] array of presigned S3 POSTs — one for every asset given as a bare filename:
image/logo src, video/audio src on animated designs, and a qrcode layer's
icon_src. When every asset is a public URL or a data URI, the import is returned already
in QUEUED with an empty uploads[] — no PUT call is needed.
The payload carries no preview image: design thumbnails are generated by the backend
after import (every format is rendered and the first visual becomes the thumbnail; the
import reports PROCESSING while previews render).
Dry run: pass ?validate_only=true to validate the payload without creating an
import. Runs the full schema and all payload-only structural validators (conflicting
filename sources, data-URI validity and decoded-size caps, TIFF type constraints,
video/audio extensions, animated keyframes, qrcode icons). Excluded: unknown_font,
project_not_found and unreachable_src.
Returns 204 No Content on success.
Plan entitlement. A design type your plan does not include is refused here rather than at generation time, so a workspace never ends up owning a design it cannot render:
type |
Requires | Available from |
|---|---|---|
static |
— | any plan |
animated |
MP4 | Pro |
printer, printer_multipage |
Printable PDF | Suite |
These are the same entitlements that gate generating the corresponding assets, so a plan that can render an animated design can also import one. See abyssale.com/pricing.
A workspace without the feature gets 429 feature_not_in_plan. The check also applies to
?validate_only=true, so a dry run never returns 204 for a design the real call refuses.
| validate_only | boolean Default: false When |
| strict | boolean Default: false When It never changes this response. Text fitting is measured while the design is
built, long after this call has answered, so |
| name required | string [ 1 .. 100 ] characters |
| type required | string Enum: "static" "printer" "animated" "printer_multipage" Design type. Muting is spelled differently on each of the three surfaces, and the type changes
too. Importing takes |
object Where the import lands. An import creates a design — the thing you can generate
from — so this defaults to Pass | |
Array of objects (DesignImportFormat) [ 1 .. 20 ] items List of formats (1-20). Required on | |
Array of objects (DesignImportLayer) [ 1 .. 80 ] items Required on | |
| unit | string Enum: "mm" "in" Print designs only ( |
| width | number > 0
|
| height | number > 0
|
| bleed_size | number >= 0
|
| safe_size | number >= 0
|
| dpi | integer [ 10 .. 300 ]
|
Array of objects (DesignImportPage) [ 1 .. 30 ] items
| |
object Animated designs only — required when |
{- "name": "Summer sale",
- "type": "static",
- "target": {
- "kind": "design",
- "project_uuid": "9d1f2b7c-5a44-4c3e-9f21-0b8e6d4a1c73"
}, - "formats": [
- {
- "name": "facebook-post",
- "width": 1200,
- "height": 1200,
- "background_color": "#F3F3F3"
}
], - "layers": [
- {
- "name": "hero",
- "type": "image",
- "layout": {
- "x": 0,
- "y": 0,
- "width": 1200,
- "height": 700
}, - "properties": {
- "src": "hero.jpg",
- "fitting_type": "cover"
}
}, - {
- "name": "title",
- "type": "text",
- "layout": {
- "x": 80,
- "y": 760,
- "width": 1040,
- "height": 220
}, - "properties": {
- "payload": "Summer sale — 40% off",
- "color": "#111111",
- "font_size": 72,
- "text_align": "center",
- "vertical_align": "middle"
}
}
]
}{- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "status": "WAITING_FOR_VALIDATION",
- "uploads": [
- {
- "target": "layer",
- "name": "hero",
- "page": "page_1",
- "fields": {
- "property1": "string",
- "property2": "string"
}, - "max_bytes": 20000000,
- "expires_at_ts": 1749831334
}
], - "version": "v2026-08-17"
}Alpha. This API is in Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. It is not covered by deprecation policy.
Poll the status of a JSON template import. Scoped to the caller's company.
Returns next_check_after_ms (suggested delay before the next poll, null on terminal states),
a stable append-only warnings[] array, and a structured error object when status is FAILED.
Expired upload windows. An import that reaches WAITING_FOR_VALIDATION and is never
validated before its presigned upload window closes is reported as terminal:
status: "FAILED", next_check_after_ms: null, no validate link, and
error: {path: "uploads", code: "missing_assets", ...}. This is derived at read time from
the import's creation timestamp — the GET performs no write, so the import keeps its
original state. A poller should therefore always drive off terminal states rather than
waiting indefinitely.
| importId required | string <uuid> Import UUID returned by the POST endpoint. |
{- "id": "c6cddf2b-d4b2-4eae-be8d-7eacd86ace01",
- "name": "Spring campaign",
- "status": "DONE",
- "created_at_ts": 1786455103,
- "validated_at_ts": 1786455103,
- "next_check_after_ms": null,
- "links": {
}, - "warnings": [ ],
- "error": null,
- "result": {
- "kind": "design",
- "uuid": "10feccaa-f7d3-491c-af21-de442a844b17",
- "project_uuid": "1d1bc138-9554-11f1-be33-f1074f796ebe"
}, - "version": "v2026-08-17"
}Alpha. This API is in Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. It is not covered by deprecation policy.
Call this after every entry of uploads[] has been POSTed to S3. The edge API performs a
pre-flight HEAD on every expected key. If any are missing, it returns
422 missing_assets with the list of unfulfilled targets.
On success, the import status transitions to QUEUED and backend processing picks it up.
PUT is idempotent on QUEUED rows. PUT on a PROCESSING, DONE, or FAILED row returns
409 template_import_already_processed.
| importId required | string <uuid> Import UUID returned by the POST endpoint. |
{- "id": "64238d01-d402-474b-8c2d-fbc957e9d290",
- "status": "QUEUED",
- "next_check_after_ms": 2000,
- "version": "v2026-08-17"
}Alpha. The contract may change without notice and imports created with it are not guaranteed to remain compatible. Not covered by the deprecation policy.
Return an existing design — any type, printer_multipage included — serialized into the
exact shape POST /designs/import/json accepts. Useful as a worked example for an agent,
or as a round-trip regression test (as-import → POST → as-import should diff empty).
src values are Abyssale-hosted https:// URLs, so no asset needs re-uploading. The
response is validated against the import schema before it is returned: a design holding
anything the schema cannot express answers 400 not_round_trippable with structured
details.
Stored state the contract has no field for is dropped and named in warnings[],
never folded into a field that means something else — a re-import reproduces the design
minus what was reported, never different artwork.
Values outside the import bounds are clipped and warned about rather than failing the
export. What cannot degrade fails instead: a gradient on a print design, a
printer_multipage whose pages disagree on their unit (each page's geometry is
expressed in its own unit, so one ruler cannot describe them), an animated format outside
the 1920×1200 box, and an export left with no layers.
Rate limit: shares the write budget with POST /designs/import/json — 60
requests/minute and 1 200/hour across both, so a round trip costs two calls and 1 200/hour
is 600 export–edit–import cycles. See Rate limits.
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
{- "payload": {
- "name": "My YouTube Template",
- "type": "static",
- "target": {
- "kind": "design",
- "project_uuid": "1de37675-f868-4726-bb1d-b08d89f463f9"
}, - "formats": [
- {
- "name": "youtube-thumbnail",
- "width": 1280,
- "height": 720,
- "background_color": "#ffffff",
- "bleed_size": 0,
- "safe_size": 0,
- "dpi": 300
}
], - "layers": [
- {
- "name": "title",
- "type": "image",
- "hidden": false,
- "locked": false,
- "layer_ids": [
- "headline",
- "subtitle"
], - "layout": {
- "youtube-thumbnail": {
- "x": 57,
- "y": 67,
- "width": 715,
- "height": 383
}
}, - "properties": {
- "default": {
- "payload": "Hello World",
- "color": "#ffffff",
- "font_size": 108
}, - "youtube-thumbnail": {
- "font_size": 80
}
}, - "animation": {
- "start_at_s": 0,
- "end_at_s": 0,
- "tweens": [
- {
- "type": "fade",
- "keyframes": [
- {
- "attr": "opacity",
- "data": {
- "type": "start",
- "time": 0
}
}, - {
- "attr": "opacity",
- "data": {
- "type": "end",
- "time": 1.5
}
}
]
}
]
}
}
], - "unit": "mm",
- "width": 210,
- "height": 297,
- "bleed_size": 0,
- "safe_size": 0,
- "dpi": 300,
- "pages": [
- {
- "background_color": "cmyk(0,0,0,0)",
- "layers": [
- {
- "name": "title",
- "type": "image",
- "hidden": false,
- "locked": false,
- "layer_ids": [
- "headline",
- "subtitle"
], - "layout": {
- "youtube-thumbnail": {
- "x": 57,
- "y": 67,
- "width": 715,
- "height": 383
}
}, - "properties": {
- "default": {
- "payload": "Hello World",
- "color": "#ffffff",
- "font_size": 108
}, - "youtube-thumbnail": {
- "font_size": 80
}
}, - "animation": {
- "start_at_s": 0,
- "end_at_s": 0,
- "tweens": [
- {
- "type": "fade",
- "keyframes": [
- {
- "attr": "opacity",
- "data": {
- "type": "start",
- "time": 0
}
}, - {
- "attr": "opacity",
- "data": {
- "type": "end",
- "time": 1.5
}
}
]
}
]
}
}
]
}
], - "animation": {
- "duration": 8,
- "screenshot_at_s": 8
}
}, - "warnings": [
- {
- "message": "string",
- "code": "color_converted",
- "path": "layers[2].properties.color",
- "layer": "headline"
}
], - "version": "string"
}Dynamic images are live image URLs that render on-the-fly based on query parameters. Once created for a design, the URL can be embedded anywhere — emails, websites, social posts — and customized with different text, colors, or images per use case, without additional API calls. Ideal for personalization at scale.
The one endpoint below mints the URL. Everything after that is the URL itself, which is
served from img.abyssale.com and is not part of this API — so its grammar is documented
here rather than under an operation.
https://img.abyssale.com/{dynamicImageId}[/{formatNameOrUid}]?{overrides}
{dynamicImageId} is the id from the creation response — not the design id. The
dynamic_image_url that response returns already includes the format segment, so you can
append a query string to it directly. Omit the format segment and the design's first format
is rendered; give a format name or its uid (prefer uid — it survives a rename).
An unknown format answers 404.
The URL is public. No API key, no signature: anyone holding it can render variants.
That is the point — it goes in an email — but it means the design, not the URL, is the
trust boundary. enable_rate_limit on the creating call caps each visitor at 5 distinct
variants per 24 h.
Overrides. Each query parameter names a layer of the design:
| Form | Syntax | Example |
|---|---|---|
| Long | layer_name.property_name=value |
title.color=%23FF0000 |
| Short | layer_name=value — the property is inferred from the layer's type |
title=Welcome |
The short form sets payload on text / button / qrcode layers, image_url on
image / logo, and rating_score on rating. Every property listed under
Element Properties
is addressable in the long form.
Five rules worth knowing before you generate URLs programmatically:
# becomes %23; spaces and nested URLs must be escaped. An
empty value is a hard 400.400.\n in a text value becomes a real line break.Note the asymmetry: unknown layers fail silently, unknown properties fail loudly. Render the URL once and look at it before wiring it into a campaign.
Creates a dynamic image URL for a given design (static designs only). Only one dynamic image is allowed per design — subsequent calls return the existing dynamic image (200 instead of 201). enable_rate_limit limits each visitor (IP + browser fingerprint) to 5 distinct variants per 24 h. enable_production_mode switches from test mode (variants saved to the workspace, server-side cache, 10 req/s per image) to production mode (built for scale: unlimited, unthrottled generation — only workspace credits and bandwidth apply; variants not saved, every request rendered fresh).
| designId required | string <uuid> Unique identifier (UUID) of the design. A value that is not a UUID does not match the
route and answers |
| enable_rate_limit | boolean Default: false Enable API rate limiting for this dynamic image |
| enable_production_mode | boolean Default: false Enable Production mode for this dynamic image |
{- "enable_rate_limit": true,
- "enable_production_mode": true
}{- "id": "a12b3c4d-5e6f-7890-abcd-ef1234567890",
- "design_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
- "formats": [
- {
- "id": "one-format",
- "uid": "11111111-2222-3333-4444-555555555555",
- "width": 3333,
- "unit": "px",
- "height": 666,
}, - {
- "id": "another-format",
- "uid": "66666666-7777-8888-9999-000000000000",
- "width": 1920,
- "unit": "px",
- "height": 1080,
}
]
}