Create Import
Alpha
The contract may change without notice, and imports created with it are not guaranteed to remain compatible. Not covered by the deprecation policy.
The first phase of a design import. You submit the full design structure (formats + layers) as JSON. The API validates the schema, stores the structure, and returns the import id, a links block, and a flat uploads[] array — one entry per asset you must upload.
Copy-paste starter
Paste this in a terminal with your API key exported. It needs no assets and no project — it creates a workspace template out of a coloured rectangle and a line of text, so it exercises the whole endpoint without the upload phase.
curl -X POST https://api.abyssale.com/designs/import/json \
-H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My first import",
"type": "static",
"target": { "kind": "design", "project_uuid": "'"$PROJECT_UUID"'" },
"formats": [
{ "name": "square", "width": 1080, "height": 1080, "background_color": "#0A363A" }
],
"layers": [
{
"name": "badge",
"type": "shape",
"layout": { "x": 90, "y": 380, "width": 900, "height": 320 },
"properties": {
"shape": "rectangle",
"background_color": "#F5C518",
"radius": { "tl": 24, "tr": 24, "br": 24, "bl": 24 }
}
},
{
"name": "title",
"type": "text",
"layout": { "x": 140, "y": 440, "width": 800, "height": 200 },
"properties": {
"payload": "Hello from the API",
"color": "#0A363A",
"font_size": 96,
"text_align": "center"
}
}
]
}'You get back an id, a links block and an empty uploads[] — no assets means nothing to upload and no PUT to make, so the import goes straight to QUEUED. Poll links.status until DONE.
Three things that trip people up, all visible above:
- A layer's own kind is
"type"at the layer level ("type": "shape"), and a shape's geometry is also"type", insideproperties. They are different fields; both are shown. "target"names the project the design lands in and is required; pass{ "kind": "workspace_template" }instead for a reusable blueprint — see Target.- To check a payload without creating anything, append
?validate_only=true; a valid body answers204 No Content.
Once this works, the full example below adds an image layer — which is what introduces the upload phase.
Query parameters
| Parameter | Type | Description |
|---|---|---|
validate_only | boolean | Validate the payload without creating an import. Answers 204 No Content. See below. |
strict | boolean | Defaults to false. When true, text overflow becomes fatal: a text layer that cannot fit its bounding box even at min_font_size fails the import instead of emitting a text_fit_failed warning. This never changes the POST's own response: text fitting is measured while the design is built, long after the POST has answered, so the failure surfaces on the status poll as status: FAILED with error.code: text_fit_failed, never as a 400. |
validate_only
A dry run creates nothing — no database row, no presigned URLs, no notifications — and answers 204 No Content on success or the standard error response on failure. Use it to iterate on a payload before committing.
It covers the full schema, every cross-field structural rule, and every check that is a pure function of the payload. Exactly three checks are skipped, each needing a round-trip a dry run does not make:
| Skipped check | Needs |
|---|---|
unknown_font | A font-catalogue lookup |
unreachable_src | A HEAD request to your URL |
project_not_found | A project lookup |
So a 204 means everything except those three passed.
The plan check is not skipped: a dry run of an animated or print design your plan does not cover answers 429, not 204.
Headers
| Header | Required | Description |
|---|---|---|
x-api-key | Yes | Company API key. |
No idempotency
POST is not idempotent. On a network timeout or 5xx, do not auto-retry blindly — check the dashboard, or poll status for the import you may already have created. A duplicate WAITING_FOR_VALIDATION row is harmless: its presigned upload URLs expire after 1 hour, and stale imports may be cleaned up automatically in the future.
For replay-safety, persist the id from the first response and resume from PUT / polling rather than re-POSTing.
Plan requirements
Some design types need a feature your plan may not include. The check runs on the POST, before anything is created, so you never end up owning a design your workspace cannot render.
type | Requires | Available from |
|---|---|---|
static | Nothing | any plan |
animated | MP4 | Pro |
printer | Printable PDF | Suite |
printer_multipage | Printable PDF | Suite |
These are the same entitlements that gate generating the corresponding assets, so any plan that can render an animated design can also import one.
A workspace without the feature gets 429 with id: "feature_not_in_plan":
{
"id": "feature_not_in_plan",
"message": "Animated designs are not included in your plan, please update your plan."
}There is no errors[] array — the payload is fine, the plan is the problem. The same check applies to ?validate_only=true, so a dry run never returns 204 for a design the real call would refuse.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name. |
type | string | Yes | Design type — one of "static" (single image), "animated" (video design with an animation timeline — see Animated Imports), "printer" (single-page print PDF, physical units + CMYK — see Print Imports), or "printer_multipage" (multi-page print PDF — see Multi-Page Print Imports). |
target | object | Effectively yes | Where the import lands. Defaults to { "kind": "design" }, and a design requires project_uuid — so omitting the block fails. See Target. |
formats | array<object> | Yes¹ | One entry per format — 1 to 20 entries. A count outside that range is rejected with out_of_range and expected: { "min": 1, "max": 20 }. See below. |
layers | array<object> | Yes¹ | Top-level layers shared across all formats — 1 to 80 entries. An empty array is rejected with out_of_range and expected: { "min": 1, "max": 80 } (also true of animated payloads and of each pages[i].layers on printer_multipage). See below. |
animation | object | On animated | Timeline object — required on type: "animated", rejected on other types. See Animated Imports. |
pages | array<object> | On printer_multipage | Ordered pages — replaces formats + layers on type: "printer_multipage". Each page carries only background_color + layers; the print setup (unit, width, height, and optionally bleed_size, safe_size, dpi) is declared once at the top level. See Multi-Page Print Imports. |
¹ formats and layers are required on static / animated / printer. On printer_multipage, omit them and supply top-level print setup + pages[] instead.
Unknown fields vs unknown query parameters
Unknown body fields are rejected with unknown_field — the schema is closed at every level. Unknown query parameters are silently ignored, as is normal for HTTP. Don't generalise from one to the other.
Target
The target block says where the import lands. An import creates a design, so this defaults to { "kind": "design" } — and because a design lives in a project, project_uuid is required.
target is a top-level field of the request body, a sibling of name and layers — not something you merge into the payload's other blocks:
{
"name": "My design",
"type": "static",
"target": { "kind": "design", "project_uuid": "3f5a7b16-6d75-4a89-afda-74b41d15d4b9" },
"formats": [ … ],
"layers": [ … ]
}target.project_uuid is required
Omitting target entirely — or sending { "kind": "design" } on its own — fails with missing_required at target.project_uuid. A design that belongs to no project is not returned by GET /designs, so there is no sensible default to fall back on. Get a project UUID from GET /projects.
The one payload that needs no project is { "kind": "workspace_template" }.
| Field | Type | Required | Description |
|---|---|---|---|
kind | enum | No | "design" (default) or "workspace_template". |
project_uuid | string (uuid) | For a design | UUID of the destination project. It must belong to the calling company and be in the CREATED state. Forbidden when kind = "workspace_template" (returns unknown_field). |
Errors:
missing_required—project_uuidis absent on a design import (including whentargetis omitted entirely).project_not_found—project_uuiddoes not exist, is not owned by the calling company, or is not inCREATEDstate. Returned with400, like every other POST body problem: it is a payload-field error reported attarget.project_uuid.unknown_field—project_uuidwas supplied withkind = "workspace_template".unknown_enum_value—kindis anything other thandesignorworkspace_template. In particulartemplateis not accepted: writeworkspace_template.
Round-trip via GET /designs/{uuid}/as-import works for designs — workspace templates are not addressable through the Designs API.
Format object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique slug-safe format name, 1–100 chars: lowercase letters, digits, dashes and underscores, starting with a letter or digit (anything else → invalid_payload). Used as the key inside layer layout / properties. The literal name default is reserved (→ reserved_format_name) because it is the merge sentinel inside those keyed objects. |
width | number | Yes | Format width. On static: integer pixels (1–5000). On printer: float in the format's unit. |
height | number | Yes | Format height. Same rules as width. |
background_color | color | Yes | Background fill. On static: #RRGGBB or #RRGGBBAA. On printer: a cmyk() / cmyka() token (a solid hex is converted for you) — see Colors & Gradients. |
unit | enum | On printer | "mm" or "in". Printer only — rejected on static (unsupported_for_type). See Print Imports. |
dpi | integer | No | Printer only. Per-format render resolution, ≤ min(300, area-based cap); omitted → the cap is used as the default. Rejected on static / animated (unsupported_for_type). On printer_multipage dpi is instead one global top-level field. See Print Imports → Render DPI. |
bleed_size | float | No | Printer only. Bleed size in the format's unit; presence enables bleed. |
safe_size | float | No | Printer only. Safe-zone size in the format's unit; presence enables the safe zone. |
No preview_image field
Formats carry no preview image — a payload containing preview_image is rejected with unknown_field at formats[i].preview_image. Thumbnails are generated automatically by the backend after import, and the status flips to DONE once they are ready. If you integrated against an earlier version of this API that required preview_image, remove the field from your payloads.
Layer object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique layer name, 1–40 chars. Used as the key inside the uploads[] response, and as the reference used by a group's layer_ids. On printer_multipage, names must be unique within a page and may repeat across pages. |
type | string | Yes | One of text, image, logo, shape, button, rating, qrcode, group, illustration (plus video / audio on animated only). See Layer Types for the full property reference of each. |
layer_ids | array<string> | On group | Names of the member layers. Sits on the layer, not in properties — see group. unsupported_for_type on any other layer type. |
layout | object | Yes | Bounding box. Flat {x, y, width, height} or keyed by format name; a single-format design may use either, a multi-format design must key. |
properties | object | Yes | Layer parameters. Same flat-vs-keyed shape as layout. See Layer Types for the shape per type. |
hidden | boolean | object | No | Hide the layer in the rendered output. Defaults to false. Either one boolean for every format, or an object keyed by format name when the layer is hidden in some formats and visible in others — see Per-format visibility. |
locked | boolean | object | No | Lock the layer in the editor. Defaults to false. Same flat-or-format-keyed shape as hidden. |
Coordinate system
layout uses {x, y, width, height} (matching GET /designs/{uuid}) — not top/right/bottom/left. The origin is the top-left corner of the format. All four fields are required.
Values are integer pixels on static / animated designs — x / y in −19999–19999, width / height in 1–19999 — and floats in the format's unit (mm/in) on printer / printer_multipage, where the same bounds apply to the pixel equivalent at 300 DPI (see Print Imports → Size limits).
Per-format visibility
A layer can be hidden in one format and visible in another. Send hidden (or locked) as an object keyed by format name to express that:
{
"name": "text_0",
"type": "text",
"hidden": { "a4-horizontal": false, "medium-rectangle": true },
"layout": { ... },
"properties": { ... }
}- The flat form —
"hidden": true— is still valid on a multi-format design and means every format. Unlikelayout, "hidden everywhere" is a complete answer, so keying is never forced. - A format you leave out of the object is not hidden. Add a
"default"key to set the formats you do not name. - Naming a format the payload does not declare is rejected with
unknown_format_key.
GET /designs/{uuid}/as-import emits the keyed form only when the formats actually disagree, so a design whose layers are uniformly visible exports exactly as before.
Every layer declares a type — one of nine, covered in full on Layer Types:
type | What it is |
|---|---|
text | Styled text with auto-fit font sizing. |
image | A bitmap fill, croppable and maskable. |
logo | A brand image, swappable at generation time. |
shape | A vector geometry — 26 built-ins, rectangle to heart. |
button | A text label on a filled, rounded background. |
rating | A star-rating row driven by a numeric value. |
qrcode | A generated QR code from an encoded URL or text. |
group | A container nesting child layers as one unit. |
illustration | A named SVG from a built-in library, optionally recoloured. |
Single-format vs multi-format
For a single-format design, the simplest form is flat objects:
"layout": { "x": 60, "y": 70, "width": 720, "height": 380 },
"properties": { "payload": "Hello", "color": "#ffffff", "font_size": 108 }A single-format design may also use the keyed form below, with its one format name (or "default") as the key — handy if you generate one payload shape for every design. Both mean the same thing.
For a multi-format design, both layout and properties must be keyed by format name — a flat object against several formats is ambiguous and is rejected with invalid_payload. A "default" key is merged under every per-format override, so each per-format entry only needs to declare fields that differ:
"layout": {
"default": { "x": 60, "y": 70, "width": 720, "height": 380 },
"youtube-thumbnail": { "width": 900 }
},
"properties": {
"default": { "payload": "Hello", "color": "#ffffff", "font_size": 108 },
"youtube-thumbnail": { "font_size": 132 }
}Unknown format keys (a key not present in formats[]) are rejected.
Request example
{
"name": "Holiday Campaign",
"type": "static",
"target": { "kind": "design", "project_uuid": "3f5a7b16-6d75-4a89-afda-74b41d15d4b9" },
"formats": [
{
"name": "instagram-post",
"width": 1080,
"height": 1080,
"background_color": "#0a363a"
}
],
"layers": [
{
"name": "title",
"type": "text",
"layout": { "x": 60, "y": 200, "width": 960, "height": 200 },
"properties": {
"payload": "Season's Greetings",
"color": "#ffffff",
"font_size": 108,
"text_align": "center"
}
},
{
"name": "hero",
"type": "image",
"layout": { "x": 151, "y": 462, "width": 778, "height": 500 },
"properties": {
"src": "hero.jpg",
"fitting_type": "cover",
"alignment": "middle center"
}
}
]
}Response
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Public UUID of the import. |
status | string | WAITING_FOR_VALIDATION when uploads[] is non-empty, or QUEUED when all sources were public URLs / data URIs (sync short-circuit). |
links.validate | string (url) | Call PUT on this URL after uploading every asset. Absent when status is already QUEUED. |
links.status | string (url) | Poll GET on this URL. |
links.dashboard | string (url) | Where to view the result. Present for a workspace template import from creation. A design import has no dashboard link until it succeeds — the design does not exist yet — and then it points at the design itself (/designs/{designId}). |
uploads | array<object> | One entry per asset that must be uploaded. Empty when no uploads are needed. |
Upload descriptor
| Field | Type | Description |
|---|---|---|
target | string | Always "layer" — every upload is a layer asset (image / logo, plus video / audio on animated). |
name | string | Name of the layer the asset belongs to. |
page | string | printer_multipage only. Which page the layer lives on (page_1 … page_N, matching array order). Present because layer names may repeat across pages — use it to disambiguate which binary goes where. Absent on all other types. |
url | string (url) | Presigned S3 endpoint to POST the binary to. |
fields | object | Form fields that must be replayed verbatim, in order, before the file part. Normally includes Content-Type, derived from your src filename's extension. |
max_bytes | integer | Upload size limit. 500 MB (500000000) on printer and printer_multipage; 20 MB (20000000) on static. On animated it depends on the layer type: 100 MB video, 25 MB audio, 20 MB image / logo. Above the cap, use a public https:// URL src instead. |
expires_at_ts | integer | Unix timestamp at which the presigned URL stops accepting uploads. |
Response example
{
"id": "1a8b2c3d-4869-11f0-96f2-0a00d9eb8f78",
"status": "WAITING_FOR_VALIDATION",
"links": {
"validate": "https://api.abyssale.com/designs/import/json/1a8b2c3d-4869-11f0-96f2-0a00d9eb8f78",
"status": "https://api.abyssale.com/designs/import/json/1a8b2c3d-4869-11f0-96f2-0a00d9eb8f78",
"dashboard": "https://app.abyssale.com/workspace-templates?importId=1a8b2c3d-4869-11f0-96f2-0a00d9eb8f78"
},
"uploads": [
{
"target": "layer",
"name": "hero",
"url": "https://<presigned-upload-host>",
"fields": {
"key": "<presigned-object-key>",
"Content-Type": "image/jpeg",
"policy": "...",
"x-amz-signature": "..."
},
"max_bytes": 20000000,
"expires_at_ts": 1749831334
}
]
}Unguessable extensions
Content-Type is derived from the src filename's extension. For the rare filename whose extension cannot be mapped to a media type, the field is omitted and the upload policy falls back to a starts-with prefix condition instead — in that case you must supply a Content-Type form part yourself, matching the layer's media prefix (image/ on image/logo, video/ on video, audio/ on audio). Whenever Content-Type is present in fields, just replay it like every other field. See Upload Assets.
src resolution
The src field on image and logo layers accepts three forms — each behaves differently in the response:
src value | Behavior |
|---|---|
Filename (e.g. "hero.jpg") | An entry is added to uploads[]. You must upload the asset before calling PUT. Max size: 20 MB on static; 500 MB on printer / printer_multipage; on animated it depends on the layer type — 100 MB video, 25 MB audio, 20 MB image / logo. A qrcode layer's icon_src caps at 1 MB on every design type. See Limits. |
Public https:// URL | No upload entry. The backend validates reachability with a HEAD request during POST and fetches the asset itself. Returns unreachable_src if the URL is not reachable. |
Data URI (data:image/...;base64,...) | Decoded and uploaded to storage server-side during the POST — no upload entry, no presigned step, nothing more to do. Max decoded size: 2 MB, on any design type (out_of_range beyond). |
Request body limit
The overall request body is limited to 20 MB (platform limit), and base64 inflates payloads — which is why inline data-URI images are capped at 2 MB decoded. For anything larger, use a filename src with the presigned upload flow, or a public https:// URL.
For image and logo layers, one shared upload entry is returned regardless of how many formats use the layer — the same asset feeds every format. A multi-format layer may therefore declare at most one distinct filename src; use https:// URLs if each format needs a different asset (two different filenames on one layer are rejected with invalid_payload).
There is no deduplication across layers, though: uploads[] is addressed by layer, so the same filename on N layers yields N entries, N storage keys and N stored copies. On printer_multipage the key is (page, layer), so a shared asset multiplies by page count too. When several layers need the same binary, reference it by public https:// URL instead.
Static and animated assets are re-encoded
On static / animated, uploaded images are normalised for the render pipeline: an opaque image is stored as JPEG, and an image with transparency as lossless WebP. Transparency is never lost, but the extension of the URL returned by GET /designs/{uuid}/as-import may differ from the filename you uploaded — don't key anything off it. On printer / printer_multipage the original is stored byte-for-byte instead (see Print Imports → Images).
Colors
Color and gradient rules moved to their own page — see Colors & Gradients.
Next step
Once you have the uploads response, upload each asset binary — see Upload Assets. If uploads[] was empty, skip straight to polling the Import Status.
