<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Abyssale API Changelog</title>
    <link>https://developers.abyssale.com/rest-api/changelog</link>
    <description>Breaking changes, additions, fixes and deprecations to the Abyssale REST API, newest first. The API is versioned by release date (vYYYY-MM-DD).</description>
    <language>en</language>
    <lastBuildDate>Thu, 24 Sep 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://developers.abyssale.com/rss/api.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>v2026-09-24</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-09-24</guid>
      <pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate>
      <description>No contract-breaking changes. Two fixes change rendered output: button shadow_* now shadows the box, and a diagonal gradient on a non-square element now renders at the angle the editor shows. Added (7): A button element takes an icon at generation time; The design import understands a button icon; A button's icon can be sent as base64: icon_encoded; A button takes a label shadow; and 3 more. Changed (4): A missing preview on a design import is reported per format, with the reason; The design import checks its https:// sources in parallel, within 10 seconds; A button layer's radius may be per corner; A gradient stop is stricter on the design import. Fixed (7): The five auto-focus settings can be sent at the top level; A button's stroke_width, text_align and text_transform are validated; A rating layer's star_color and background_color no longer accept a gradient; …</description>
      <content:encoded><![CDATA[<p>No contract-breaking changes. Two fixes change rendered output: button <code>shadow_*</code> now shadows
the box, and a diagonal gradient on a non-square element now renders at the angle the editor shows.</p>
<h3>Added</h3>
<ul>
<li>
<p><strong>A <a href="https://developers.abyssale.com/rest-api/generation/element-properties/button#the-label-icon"><code>button</code> element takes an icon</a>
at generation time.</strong> <code>icon_url</code> is a public HTTP(s) URL of the image to place beside the label,
<code>icon_color</code> recolours it. Either can be sent on its own, and a button whose design already
carries an icon keeps it when neither is.</p>
<p><strong>The icon's geometry belongs to the design, not the request</strong> — where it sits, how big it is and
how far it stands from the label are set in the editor, the same way a text layer's font family
is. <code>icon_color</code> only bites on an <strong>SVG</strong>: recolouring rewrites the paint inside the file, and a
raster has none to rewrite.</p>
</li>
<li>
<p><strong>The <a href="https://developers.abyssale.com/rest-api/designs/import/layer-types/button#the-label-icon">design import</a> understands a
button icon.</strong> <code>icon_src</code> takes the three forms <code>src</code> already accepts — a filename you upload, an
<code>https://</code> URL, or a data URI — beside <code>icon_color</code>, <code>icon_size</code>, <code>icon_gap</code> and <code>icon_alignment</code>.</p>
<p><strong><code>icon_size</code> is a percentage of the label's font size, not a length</strong> (10–200, default <code>100</code>), so
the icon keeps its proportion when the label auto-fits. <code>icon_gap</code> <em>is</em> a length.</p>
</li>
<li>
<p><strong>A <a href="https://developers.abyssale.com/rest-api/generation/element-properties/button#the-label-icon"><code>button</code>'s icon can be sent as base64</a>:
<code>icon_encoded</code>.</strong> The alternative to <code>icon_url</code> when you hold the bytes rather than a URL — the
twin of <code>image_encoded</code>, taking a bare base64 body or a <code>data:image/&lt;type&gt;;base64,</code> data URI, with
the same file types and size cap. <code>icon_url</code> wins if both are sent, and the value is never stored
on the generation request. <em>It already reached the renderer undeclared, so a data URI failed the
generation with a bare <code>500</code>; a value that is not an image is now refused with <code>invalid_payload</code>.</em></p>
</li>
<li>
<p><strong>A <a href="https://developers.abyssale.com/rest-api/generation/element-properties/button#two-shadows"><code>button</code> takes a label shadow</a></strong> —
<code>text_shadow_color</code>, <code>text_shadow_blur</code>, <code>text_shadow_offset_x</code> and <code>text_shadow_offset_y</code> shadow
the <strong>label</strong>, next to the <code>shadow_*</code> family that shadows the <strong>box</strong>. A button is the one layer
with two shadows, and both can be set at once.</p>
</li>
<li>
<p><strong>A linear gradient may carry 2 to 8 colour stops.</strong> The grammar
<code>linear-gradient(&lt;x&gt;% &lt;y&gt;% &lt;x1&gt;% &lt;y1&gt;%, …)</code> now takes up to eight
<code>&lt;offset&gt;% &lt;color&gt; &lt;alpha&gt;</code> stops instead of exactly two, everywhere a gradient is accepted —
at generation time and on the design import alike.</p>
<p>Each stop keeps the offset it was authored at, so a ramp written at <code>0/20/75/100</code> renders at
those positions rather than being re-spaced evenly. Two stops stay valid and unchanged: this
widens the grammar, it does not move it. A ninth stop is rejected with <code>invalid_payload</code>. On a
printer design the extra stops follow the same rules as the first two — <code>cmyk(C,M,Y,K)</code> or
<code>#RRGGBB</code>, and every one of them opaque.</p>
</li>
<li>
<p><strong>A printer design accepts a linear gradient on a background.</strong> Every <code>linear-gradient(...)</code>
used to be refused on a <code>printer</code> / <code>printer_multipage</code> design, because the print pipeline
collapsed it to its first colour and would have shipped a flat fill instead of the ramp you
authored. The whole stack now carries a real gradient — every stop converted, an axial shading
in the PDF — so it is accepted in the one place print can draw one: <strong><code>background_color</code> of a
<code>shape</code> or <code>button</code> layer</strong>.</p>
<p>Stops may be written <code>cmyk(C,M,Y,K)</code> or <code>#RRGGBB</code>; a hex stop is converted per stop with a
<a href="https://developers.abyssale.com/rest-api/designs/import/status-warnings-and-errors#warnings"><code>color_converted</code></a> warning.
Duplicating a static design to print follows the same rule: a gradient on a shape or button
background is kept, each stop converted; one on the format background or a text colour is
flattened to its first stop.</p>
<p><strong>Every stop must be opaque.</strong> A PDF shading carries no alpha channel, so a stop with an alpha
below <code>1</code> is refused rather than flattened onto white — that would be a colour you never asked
for. A gradient anywhere else on print is still refused: a text <code>color</code>, a <code>star_color</code>, a
border, a page or format background.</p>
</li>
<li>
<p><strong><a href="https://developers.abyssale.com/rest-api/designs/as-import"><code>GET /designs/{designId}/as-import</code></a> returns the icon</strong>, so a
design built in the editor with a button icon round-trips unchanged.</p>
</li>
</ul>
<h3>Changed</h3>
<ul>
<li>
<p><strong>A missing preview on a <a href="https://developers.abyssale.com/rest-api/designs/import/status-warnings-and-errors#warnings">design import</a>
is reported per format, with the reason.</strong> <code>preview_generation_failed</code> used to be one entry at
<code>formats</code> — &quot;1 of 4 preview(s) failed to render&quot; — naming neither the format nor the cause. There
is now one entry per format without a preview, at <code>formats[i]</code> (<code>pages[i]</code> on a multipage
document), with a new <code>reason</code> field: <code>render_failed</code> (the <code>message</code> quotes why when it is about
the design, so the same payload will fail the same way) or <code>preview_not_stored</code> (the format
rendered but its preview could not be stored — this case used to go unreported, leaving a
<code>DONE</code> import whose <code>preview_url</code> answered an error).</p>
</li>
<li>
<p><strong>The <a href="https://developers.abyssale.com/rest-api/designs/import/create">design import</a> checks its <code>https://</code> sources in parallel,
within 10 seconds.</strong> Each distinct <code>src</code> URL used to be checked one after another, so a payload
with about ten slow hosts could run past the 29-second gateway limit: the call answered
<code>500 Endpoint request timed out</code>, yet the import could still be created. A source that has not
answered within 10 seconds of the start of the check is now reported as <code>unreachable_src</code>.</p>
</li>
<li>
<p><strong>A <code>button</code> layer's <code>radius</code> may be per corner</strong> — <code>{ tl, tr, br, bl }</code> as well as a single
number, exactly as <code>shape</code>, <code>image</code> and <code>logo</code> already allowed. A button whose corners differ is
now <strong>exported that way</strong> too: the export previously emitted only the top-left corner, so
re-importing a design with three square corners rounded all four. A uniform radius is still
emitted as a single number, so payloads that never used per-corner radii are unchanged.</p>
</li>
<li>
<p><strong>A gradient stop is stricter on the <a href="https://developers.abyssale.com/rest-api/designs/import/colors">design import</a>.</strong> A stop's
alpha must lie between <code>0</code> and <code>1</code>, and a <code>cmyk(C,M,Y,K)</code> stop is written without spaces — the
one form every renderer reads. Either used to be accepted and slip past the print opacity check;
both now answer <code>invalid_payload</code>. A gradient with <code>cmyk(...)</code> stops on a static or animated
format background is refused with <code>unsupported_for_type</code>, as it already was on a layer.</p>
</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>
<p><strong>The five auto-focus settings can be sent at the top level</strong>, beside <code>auto_focus</code>, as well as
inside <code>auto_focus_properties</code> — <code>auto_focus_model</code>, <code>focus_objects</code>, <code>focus_framing</code>,
<code>focus_zoom</code> and <code>focus_target</code>. They were already accepted there but documented nowhere, and
<code>auto_focus_model</code> was silently ignored when sent that way. It is now honoured, with
<code>auto_focus_properties.model</code> still winning when both are sent.</p>
</li>
<li>
<p><strong>A <code>button</code>'s <code>stroke_width</code>, <code>text_align</code> and <code>text_transform</code> are validated.</strong> All three are
documented button properties, but a value outside the documented set was forwarded to the
renderer rather than refused — <code>text_align: &quot;middle&quot;</code> was accepted and silently did nothing.</p>
</li>
<li>
<p><strong>A <a href="https://developers.abyssale.com/rest-api/generation/element-properties/rating"><code>rating</code></a> layer's <code>star_color</code> and
<code>background_color</code> no longer accept a gradient.</strong> The published pattern said they did, and the
design import took one — but a rating is drawn as filled polygons and no renderer can shade it,
so the generation failed with a bare <code>500</code> and the preview never arrived. Both fields now take
<code>#RRGGBB</code> / <code>#RRGGBBAA</code> or <code>cmyk(...)</code> / <code>cmyka(...)</code>, and a gradient is refused at import with
<code>invalid_payload</code>. Generation-time overrides already refused one. A design carrying such a
gradient cannot render today, so no working payload loses anything.</p>
</li>
<li>
<p><strong><code>shadow_*</code> on a <code>button</code> now shadows the button, not its label.</strong> A shadow sent for the button
was painted behind the text — a second, offset copy of the label — and the box got none. If you
relied on that, send <a href="https://developers.abyssale.com/rest-api/generation/element-properties/button#two-shadows"><code>text_shadow_*</code></a>
instead.</p>
</li>
<li>
<p><strong>A gradient where print cannot draw one is refused at generation.</strong> On a <code>printer</code> /
<code>printer_multipage</code> design, a gradient on the format background (<code>root</code>), a text or button-label
<code>color</code>, or a text background was accepted and then failed the generation with a bare <code>500</code>. It
is now <code>400 invalid_payload</code> at submit, naming the element. A shape or button <code>background_color</code>
still takes one.</p>
</li>
<li>
<p><strong>A diagonal gradient on a non-square element rendered at the wrong angle.</strong> A gradient's
coordinates are fractions of the element's box, as the editor draws them, but the image renderer
read them as pixels — which only agrees on a square. On a 400×120 button a corner-to-corner
diagonal came out 17° off. Buttons, text fills and backgrounds now render as the editor shows
them, so an existing design with such a gradient renders slightly differently; horizontal and
vertical gradients are unchanged.</p>
</li>
<li>
<p><strong>An HTML5 export dropped most of a gradient on the format background.</strong> It kept only the first
two stops, spaced them evenly and discarded their alpha. Every stop now keeps its offset and
alpha. The direction is still snapped to horizontal, vertical or a 45° diagonal.</p>
</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-09-21</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-09-21</guid>
      <pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate>
      <description>No breaking changes. Added (1): expand on the image element.</description>
      <content:encoded><![CDATA[<p>No breaking changes.</p>
<h3>Added</h3>
<ul>
<li><strong><a href="https://developers.abyssale.com/rest-api/generation/element-properties/image#expand"><code>expand</code> on the image element</a></strong>
extends an image beyond its original borders using AI, filling the empty space
<code>fitting_type: &quot;fill&quot;</code> leaves around an off-center image instead of leaving it blank. Models:
<code>flux-2-pro-outpaint</code> (default), <code>image-outpaint</code>, <code>bria-expand</code>. Ignored when <code>fitting_type</code> is
<code>cover</code>, since that mode already fills the whole bounding box. Asynchronous generation only —
rejected with <code>400 invalid_payload</code> on synchronous generation, same as <code>text_to_image</code>.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-09-02</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-09-02</guid>
      <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
      <description>No breaking changes. Added (1): GET /credits.</description>
      <content:encoded><![CDATA[<p>No breaking changes.</p>
<h3>Added</h3>
<ul>
<li><strong><a href="https://developers.abyssale.com/rest-api/credits#check-your-balance"><code>GET /credits</code></a></strong> returns your workspace's remaining
credits for the current billing period — <code>generation_credits</code> and <code>ai_credits</code>, each with
<code>available</code>, <code>limit</code>, <code>consumed</code> and <code>extra</code>. <code>available</code> and <code>limit</code> are <strong><code>null</code> on an
unlimited plan</strong>, so branch on <code>null</code> before comparing numbers. The read costs no credits and is
on the <a href="https://developers.abyssale.com/rest-api/rate-limits#read">Read</a> budget.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-08-31</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-08-31</guid>
      <pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate>
      <description>Everything below is on the Alpha design import surface (POST /designs/import/json, GET /designs/{designId}/as-import). No stable endpoint changed. As stated when that surface shipped, its contract may change without notice and is not covered by the deprecation policy — these are the changes. Breaking Action required if you author animations (3): Animation keyframe values are validated; A keyframe must sit inside its layer's visible window; GET /designs/{designId}/as-import fails on a design that breaks either rule. Added (2): A format's background_color is optional and defaults to white; A button layer's alignment accepts the single-token shorthand.</description>
      <content:encoded><![CDATA[<p>Everything below is on the <strong>Alpha</strong> <a href="https://developers.abyssale.com/rest-api/designs/import/">design import</a> surface
(<code>POST /designs/import/json</code>, <code>GET /designs/{designId}/as-import</code>). No stable endpoint changed. As
stated when that surface shipped, its contract may change without notice and is not covered by the
deprecation policy — these are the changes.</p>
<h3>Breaking <span class="sev sev--mid">Action required if you author animations</span></h3>
<ul>
<li>
<p><strong><a href="https://developers.abyssale.com/rest-api/designs/import/animated#the-six-animations">Animation keyframe values are validated</a>.</strong>
Every <code>attr</code> a tween animates used to be passed straight through, so a value the renderer cannot
use was stored and simply never played. Each is now checked, and <strong>the units are not the same for
every one</strong>:</p>
<table>
<thead>
<tr>
<th><code>attr</code></th>
<th>Range</th>
<th>Unit</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opacity</code></td>
<td><code>0</code> … <code>1</code></td>
<td><strong>fraction</strong></td>
</tr>
<tr>
<td><code>volumeEffect</code></td>
<td><code>0</code> … <code>1</code></td>
<td><strong>fraction</strong></td>
</tr>
<tr>
<td><code>scale</code></td>
<td><code>0</code> … <code>100</code></td>
<td>percent, <code>100</code> = as designed</td>
</tr>
<tr>
<td><code>angle</code></td>
<td>no bound</td>
<td>signed degrees (<code>1140</code> = four clockwise turns)</td>
</tr>
<tr>
<td><code>left</code>, <code>top</code></td>
<td>no bound</td>
<td>signed px</td>
</tr>
<tr>
<td><code>typewriting</code></td>
<td><code>0</code> or <code>100</code> only</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>A non-number is <code>invalid_payload</code>, a number outside its range is <code>out_of_range</code>, and a
<code>typewriting</code> other than <code>0</code>/<code>100</code> is <code>unknown_enum_value</code> — all at
<code>layers[i].animation.tweens[j].keyframes[k].attr.&lt;name&gt;</code>.</p>
<p><strong>Check <code>opacity</code> first.</strong> Inside a keyframe it is a <code>0</code>–<code>1</code> fraction, but a layer's
<code>properties.opacity</code> is an integer <code>0</code>–<code>100</code>. Same word, two scales, so <code>{&quot;opacity&quot;: 100}</code> in a
keyframe is now <code>out_of_range</code> rather than &quot;fully opaque&quot;. <code>volumeEffect</code> is a fraction for the
same reason, and <code>typewriting</code> is not a range at all — the effect types the text in whole, so
<code>50</code> is not &quot;half typed&quot;.</p>
</li>
<li>
<p><strong>A keyframe must sit inside its layer's visible window.</strong> <code>data.time</code> is absolute seconds on the
timeline and must fall within the layer's <code>start_at_s</code> … <code>end_at_s</code> (defaulting to <code>0</code> and
<code>animation.duration</code>), otherwise <code>out_of_range</code> at <code>…keyframes[k].data.time</code>. A layer's effects
cannot run outside the window in which the layer exists, so such a keyframe was never reached and
imported as an animation that never played. Keyframes are still not required to be ordered.</p>
</li>
<li>
<p><strong><a href="https://developers.abyssale.com/rest-api/designs/as-import"><code>GET /designs/{designId}/as-import</code></a> fails on a design that
breaks either rule.</strong> The export is validated against the import before it is returned, so a
design holding an out-of-range keyframe value answers <code>not_round_trippable</code> instead of handing
back a payload the import would reject. If you hit this on a design you did not create through
the API, its stored animation is the problem — send us the design id.</p>
</li>
</ul>
<h3>Added</h3>
<ul>
<li><strong>A format's <code>background_color</code> is optional and defaults to white</strong> — <code>#FFFFFF</code> on <code>static</code> and
<code>animated</code>, <code>cmyk(0,0,0,0)</code> on <code>printer</code>. Same for a
<a href="https://developers.abyssale.com/rest-api/designs/import/printer-multipage"><code>printer_multipage</code> page</a>. Omitting it emits no
<code>color_converted</code> warning.</li>
<li><strong>A <a href="https://developers.abyssale.com/rest-api/designs/import/layer-types/button"><code>button</code></a> layer's <code>alignment</code> accepts the
single-token shorthand</strong> every other layer type already accepted: one token names one axis and
the other takes its middle, so <code>center</code> is <code>middle center</code> and <code>top</code> is <code>top center</code>. Exports
still return the two-token form.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-08-21</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-08-21</guid>
      <pubDate>Fri, 21 Aug 2026 00:00:00 GMT</pubDate>
      <description>No breaking changes. Added (4): Webhook deliveries are signed; GET /signing-secret; POST /signing-secret/rotate; POST /signing-secret/revoke.</description>
      <content:encoded><![CDATA[<p>No breaking changes.</p>
<h3>Added</h3>
<ul>
<li><strong>Webhook deliveries are signed</strong> — every delivery carries
<code>X-Abyssale-Signature: t=&lt;unix&gt;,v1=&lt;hex&gt;</code>, an HMAC-SHA256 over
<code>&quot;v1:webhook:&quot; + t + &quot;.&quot; + raw_body</code>, plus <code>X-Abyssale-Delivery-Id</code> for deduplication. Signing is
opt-in: a workspace that has never fetched its secret keeps receiving unsigned deliveries.
Additive — no payload field and no status code changed. See
<a href="https://developers.abyssale.com/webhooks/signature-verification">Signature verification</a>.</li>
<li><strong><a href="https://developers.abyssale.com/webhooks/signature-verification#get-your-signing-secret"><code>GET /signing-secret</code></a></strong> returns the
workspace's signing secret, minting it on the first call. One secret covers every webhook in the
workspace. It is not the API key and cannot replace one.</li>
<li><strong><a href="https://developers.abyssale.com/webhooks/signature-verification#rotating-the-secret"><code>POST /signing-secret/rotate</code></a></strong> issues a
new secret and keeps the previous one valid for 24 hours, signing each delivery with both. A
second rotate inside that window answers <code>409 previous_secret_still_active</code> unless you pass
<code>?force=true</code>.</li>
<li><strong><a href="https://developers.abyssale.com/webhooks/signature-verification#if-your-secret-leaks"><code>POST /signing-secret/revoke</code></a></strong> ends the overlap early,
leaving the current secret in place.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-08-20</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-08-20</guid>
      <pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate>
      <description>No breaking changes. Fixed (3): group layers carry attributes; Repeating a query parameter no longer changes the response; Error messages name the offending field. Reference corrections (3): code, an animated-only read-only layer type, is documented; A keyframe's attr is an object ({&quot;left&quot;: 1021, &quot;top&quot;: 347}), not a string; mask_properties and filter_properties values are objects, not scalars.</description>
      <content:encoded><![CDATA[<p>No breaking changes.</p>
<h3>Fixed</h3>
<ul>
<li><strong><code>group</code> layers carry <code>attributes</code></strong> — always empty (<code>{}</code> on a <code>printer_multipage</code> page, <code>[]</code>
everywhere else). Nothing on a group is customisable; the key exists so every element has the
same shape.</li>
<li><strong>Repeating a query parameter no longer changes the response</strong> — a duplicated <code>?i=advanced</code> fell
back to the plain read, and a duplicated <code>?version=</code> answered <code>500</code>.</li>
<li><strong>Error messages name the offending field</strong> — <code>name: Missing data for required field.</code> rather
than &quot;One or more request fields are invalid.&quot;, including errors relayed from the generation
engine. <code>errors</code> and <a href="https://developers.abyssale.com/rest-api/generation/errors"><code>id</code></a> are unchanged.</li>
</ul>
<h3>Reference corrections</h3>
<p>The API is unchanged — the <a href="https://developers.abyssale.com/api-reference/">reference</a> described these wrongly,
so a client validating responses against it rejected valid payloads.</p>
<ul>
<li><code>code</code>, an <code>animated</code>-only read-only layer type, is documented. Skip it as you skip <code>container</code>.</li>
<li>A keyframe's <code>attr</code> is an object (<code>{&quot;left&quot;: 1021, &quot;top&quot;: 347}</code>), not a string.</li>
<li><code>mask_properties</code> and <code>filter_properties</code> values are objects, not scalars.</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>v2026-08-17</title>
      <link>https://developers.abyssale.com/rest-api/changelog</link>
      <guid isPermaLink="false">abyssale:api-changelog:v2026-08-17</guid>
      <pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate>
      <description>Breaking Action required (8): One error envelope on every endpoint and every status; Framework failures answer real codes; Three misspelled 404 codes are corrected; Every JSON object response carries a top-level version; and 4 more. Added (7): Design import; GET /designs/{designId}/as-import; GET /workspace-templates; Designs report their project; and 3 more. Changed (1): GET /fonts sorts case-insensitively. Deprecated Plan a migration (4): remove_bg on synchronous generation; text_to_image is gone from the synchronous schema; The category_id query parameter on GET /designs; auto_focus_properties.model: &quot;face&quot;. Fixed (5): A deleted design answers 404, not 400; No error response is empty; POST /public/asset/import answers 400, not 500, when asset_name is missing; Field-level detail always uses {path, code, message}; and 1 more.</description>
      <content:encoded><![CDATA[<h3>Breaking <span class="sev sev--high">Action required</span></h3>
<ul>
<li><strong><a href="https://developers.abyssale.com/rest-api/generation/errors">One error envelope</a> on every endpoint and every status</strong> —
<code>{id, message, errors?}</code>. <code>errors</code> is now a flat array, and the code moved to top-level <code>id</code>.
<a href="https://developers.abyssale.com/rest-api/generation/errors">Migration steps</a>.</li>
<li><strong>Framework failures answer real codes</strong> — <code>method_not_allowed</code>, <code>invalid_json</code>,
<code>internal_error</code> — instead of the placeholder <code>error</code>.</li>
<li><strong>Three misspelled 404 codes are corrected</strong>, each of which held a literal space:
<code>project_not found</code> → <a href="https://developers.abyssale.com/rest-api/generation/errors"><code>project_not_found</code></a>, and likewise for
<code>workspace_template_not_found</code> and <code>duplication_request_not_found</code>.</li>
<li><strong>Every JSON object response carries a top-level <code>version</code>.</strong> Clients that reject unknown fields
must accept it.</li>
<li><strong><code>GET /designs</code> and <code>GET /projects</code> hide deleted records</strong> — a design whose project was deleted
is no longer listed, and only <code>CREATED</code> projects are returned.</li>
<li><strong><code>message</code> no longer begins with the HTTP status number</strong> — <code>&quot;404 Design … not found&quot;</code> is now
<code>&quot;Design … not found&quot;</code>. A client that slices or matches that prefix breaks silently. Branch on
<code>id</code> instead; see <a href="https://developers.abyssale.com/rest-api/generation/errors">Errors</a>.</li>
<li><strong>An unknown format answers <code>404</code>, not <code>400</code>, on synchronous generation</strong> — matching every other
endpoint and the reference.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/rate-limits">Per-workspace rate limiting</a>, with a new <code>429</code> code.</strong> Endpoints now
carry a per-minute and per-hour budget by route class and answer
<a href="https://developers.abyssale.com/rest-api/rate-limits#three-different-429s"><code>429 request_rate_limited</code></a> when you exceed it.
This is a <strong>different <code>id</code></strong> from <code>rate_limit_exceeded</code>, which keeps its meaning — out of
credits, or the global ceiling. A client that branches on <code>rate_limit_exceeded</code> alone will not
recognise a throttle. Retry <code>request_rate_limited</code>. <code>rate_limit_exceeded</code> covers two situations
and <code>message</code> tells them apart — back off when it is the global ceiling, top up or upgrade when
it is credits or plan, which no amount of retrying fixes. <code>feature_not_in_plan</code> is never worth
retrying. Every response now carries <code>X-RateLimit-Limit</code>, <code>X-RateLimit-Remaining</code> and <code>X-RateLimit-Reset</code>,
and a refusal adds <code>Retry-After</code> — pace against those rather than retrying blind. The budget is
per <strong>workspace</strong>, so extra API keys do not raise it.</li>
</ul>
<h3>Added</h3>
<ul>
<li><strong><a href="https://developers.abyssale.com/rest-api/designs/import/">Design import</a></strong> <span class="alpha-badge">Alpha</span> —
<code>POST /designs/import/json</code> builds a design or workspace template from JSON (<code>static</code>,
<code>animated</code>, <code>printer</code>, <code>printer_multipage</code>), with <code>GET</code>/<code>PUT /designs/import/json/{importId}</code>
to poll and validate. The design type is checked against your plan <strong>before anything is created</strong>
— <code>animated</code> needs MP4, <code>printer</code> and <code>printer_multipage</code> need Printable PDF — and a workspace
without it gets a new <a href="https://developers.abyssale.com/rest-api/generation/errors"><code>429 feature_not_in_plan</code></a>, on
<code>?validate_only=true</code> as well.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/designs/as-import"><code>GET /designs/{designId}/as-import</code></a></strong>
<span class="alpha-badge">Alpha</span> — export any design in the exact shape the import accepts.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/workspace-templates"><code>GET /workspace-templates</code></a></strong> and
<code>GET /workspace-template-categories</code> list the workspace's shared templates and their categories.</li>
<li><strong>Designs report their project</strong> — <code>project_id</code> / <code>project_name</code> on the listing and detail
endpoints, plus a <code>project_id</code> filter. <code>category_id</code> / <code>category_name</code> remain as deprecated
aliases.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/designs/design-details"><code>GET /designs/{designId}</code></a> returns the full design model</strong> —
print geometry (<code>dpi</code>, <code>bleed_size</code>, <code>safe_size</code>), the animated <code>animation</code> block with per-element
timing and tweens, and <code>pages[]</code> plus <code>elements_per_page</code> (keyed <code>page_1</code>…<code>page_N</code>) for
multi-page print designs.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/designs/design-details#group-layers"><code>?i=advanced</code> returns group layers</a></strong>, with
<code>layer_ids</code>, per-format <code>layout</code>, and auto-layout settings. Without the parameter the response
keeps its previous shape.</li>
<li><strong><a href="https://developers.abyssale.com/webhooks/events/">Webhook payloads carry <code>event_type</code></a></strong> — route on it instead of inferring
the event from the payload's shape.</li>
</ul>
<h3>Changed</h3>
<ul>
<li><code>GET /fonts</code> sorts case-insensitively.</li>
</ul>
<h3>Deprecated <span class="sev sev--mid">Plan a migration</span></h3>
<ul>
<li><strong><a href="https://developers.abyssale.com/rest-api/generation/element-properties/image#remove-background"><code>remove_bg</code> on synchronous generation</a></strong>
— still works, but it can push the call past the endpoint's 10-second cap. Remove backgrounds
asynchronously. <code>auto_focus</code> is unaffected.</li>
<li><strong><code>text_to_image</code> is gone from the synchronous schema.</strong> It was always rejected there.</li>
<li><strong>The <code>category_id</code> query parameter on <a href="https://developers.abyssale.com/rest-api/designs/list-designs"><code>GET /designs</code></a></strong> — filter
with <code>project_id</code>. Existing requests keep working.</li>
<li><strong><a href="https://developers.abyssale.com/rest-api/generation/element-properties/image#the-face-model-is-deprecated"><code>auto_focus_properties.model: &quot;face&quot;</code></a></strong>
— use <code>&quot;people&quot;</code> with <code>focus_framing: &quot;face&quot;</code>. It detects faces with the same model and also
honours <code>focus_zoom</code> and <code>focus_target</code>, which <code>&quot;face&quot;</code> ignores. <code>focus_framing</code> already defaults
to <code>&quot;face&quot;</code>, so switching to <code>&quot;model&quot;: &quot;people&quot;</code> is usually the whole migration. <code>&quot;face&quot;</code> keeps
working.</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><strong>A deleted design answers <code>404</code>, not <code>400</code></strong>, on the generation endpoints, with the same
<code>template_not_found</code> code a design that never existed already returned.</li>
<li><strong>No error response is empty.</strong> <code>GET /banners/{bannerId}</code>, the banner listing, ZIP export with
unknown ids, a failed workspace-template duplication and the public renderer all returned a JSON
content type with no body.</li>
<li><strong><code>POST /public/asset/import</code> answers <code>400</code>, not <code>500</code>, when <code>asset_name</code> is missing.</strong></li>
<li><strong>Field-level detail always uses <code>{path, code, message}</code></strong> — errors relayed from the generation
engine used to arrive as <code>{field, message}</code>, and now carry an <code>id</code> rather than prose alone.</li>
<li><strong>Plan and credit gates on asynchronous generation answer
<a href="https://developers.abyssale.com/rest-api/generation/errors#rate-limits-and-plan-limits-%E2%80%94-429"><code>429 rate_limit_exceeded</code></a></strong> instead of
a <code>400</code> whose message merely started with &quot;429&quot;.</li>
</ul>]]></content:encoded>
    </item>
  </channel>
</rss>
