Node.js SDK
@abyssale/sdk is the official Node.js / TypeScript client. It is a zero-config singleton: no constructor, no client to pass around — set one environment variable and call a method.
npm install @abyssale/sdk
export ABYSSALE_API_KEY="{YOUR-API-KEY}"Requires Node.js ≥ 20. Ships both ESM and CommonJS builds, with types generated from the OpenAPI spec.
import abyssale from '@abyssale/sdk';
const { data, error } = await abyssale.generateImage('{designId}', {
template_format_name: 'facebook-feed',
elements: {
text_title: { payload: 'Summer sale — 40% off' },
},
});
if (error) console.error(error);
else console.log(data.file.cdn_url);New to the API? Quickstart walks the same path in four calls, with cURL and Python alongside.
Configuration
Configuration is environment-only, read once when the module is first imported.
| Variable | Required | Default | Description |
|---|---|---|---|
ABYSSALE_API_KEY | Yes | — | Your API key. Sent as x-api-key on every request. |
ABYSSALE_TIMEOUT_MS | No | 30000 | Per-request timeout in milliseconds. Must be a positive number. |
ABYSSALE_MAX_RETRIES | No | 3 | Maximum automatic retries — see Retries and timeouts. Must be a non-negative integer. |
The key is read at import time
With no ABYSSALE_API_KEY set, the SDK throws as it loads, before you call anything: [abyssale] ABYSSALE_API_KEY environment variable is not set. Set it in the process environment before the first import — loading a .env file after importing the SDK is too late. An invalid ABYSSALE_TIMEOUT_MS or ABYSSALE_MAX_RETRIES throws the same way.
Every method
Every method returns { data, error, response } and resolves — see Errors. Arguments below are TypeScript signatures; body shapes are the request bodies documented on each guide.
Authentication
| Method | Endpoint | Guide |
|---|---|---|
verifyApiKey() | POST /auth | Authentication |
Designs
| Method | Endpoint | Guide |
|---|---|---|
listDesigns(query?) | GET /designs | List Designs |
getDesign(designId, options?) | GET /designs/{designId} | Design Details |
getDesignFormat(designId, formatSpecifier) | GET /designs/{designId}/formats/{formatSpecifier} | Design Format Details |
listDesigns filters on project_id and type. getDesign takes { advanced: true } to include group layers, which the default response omits; getDesignFormat is always the advanced view and accepts either a format name or its UUID.
Generation
| Method | Endpoint | Guide |
|---|---|---|
generateImage(designId, body) | POST /banner-builder/{designId}/generate | Generate Single Image |
generateMultiFormatMedia(designId, body) | POST /async/banner-builder/{designId}/generate | Asynchronous Generation |
generateMultiPagePdf(designId, body) | POST /async/banner-builder/{designId}/generate-multipage-pdf | Multi-Page PDF |
getGenerationRequest(generationRequestId) | GET /generation-request/{generationRequestId} | Polling |
waitForGenerationRequest(id, options?) | polls the above | Polling helpers |
Files
| Method | Endpoint | Guide |
|---|---|---|
getFile(bannerId) | GET /banners/{bannerId} | Endpoint Catalog |
Fonts
| Method | Endpoint | Guide |
|---|---|---|
listFonts() | GET /fonts | Fonts |
Projects
| Method | Endpoint | Guide |
|---|---|---|
listProjects() | GET /projects | Projects |
createProject(body) | POST /projects | Projects |
Exports
| Method | Endpoint | Guide |
|---|---|---|
exportBanners(body) | POST /async/banners/export | Asset Export |
Dynamic images
| Method | Endpoint | Guide |
|---|---|---|
createDynamicImageUrl(designId, body) | POST /designs/{designId}/dynamic-image-url | Create a Dynamic Image |
Workspace templates
| Method | Endpoint | Guide |
|---|---|---|
listWorkspaceTemplates(query?) | GET /workspace-templates | Workspace Templates |
listWorkspaceTemplateCategories() | GET /workspace-template-categories | Workspace Templates |
duplicateWorkspaceTemplate(companyTemplateId, body) | POST /workspace-templates/{companyTemplateId}/use | Use a Workspace Template |
getDuplicationRequest(duplicateRequestId) | GET /design-duplication-requests/{duplicateRequestId} | Use a Workspace Template |
waitForDuplicationRequest(id, options?) | polls the above | Polling helpers |
Errors
The SDK never throws on an HTTP error. Every method resolves to three fields, and exactly one of data / error is populated:
| Field | When | Contents |
|---|---|---|
data | Success | The parsed response body, typed per endpoint. |
error | Any non-2xx | The API's error envelope — {id, message}, plus errors[] when the failure is field-level. |
response | Always | The raw Response. Use it for status codes and headers. |
const { data, error, response } = await abyssale.generateImage(designId, { elements: {} });
if (error) {
// Branch on the machine-readable code, never on the message text
if (error.id === 'template_not_found') throw new Error('Unknown design');
console.error(response.status, error.id, error.message);
} else {
console.log(data.file.cdn_url);
}error carries the same envelope as the REST API, so the full id catalogue on Errors applies unchanged. Field-level failures put the detail in error.errors[] as {path, code, message}.
Rate-limit headers are not parsed for you
The SDK does not surface X-RateLimit-*. Read them off response.headers when you want to pace against your remaining budget — see Rate limits.
Retries and timeouts
Failed requests are retried automatically up to ABYSSALE_MAX_RETRIES times (default 3), with exponential backoff of 1 s, 2 s, 4 s plus up to 100 ms of jitter. When the response carries Retry-After, that value is honoured instead of the backoff.
The rules are deliberately narrower than "retry every 429 and 5xx":
| Response | Retried? |
|---|---|
500, 502, 503, 504 on a GET/HEAD/OPTIONS | Yes |
500, 502, 503, 504 on a POST | Never |
429 with Retry-After | Yes, after exactly that long |
429 without Retry-After | Once, after 1 s |
429 without Retry-After, id: feature_not_in_plan | No |
| Anything else | No |
A POST is never retried for you
Every POST on this API generates an asset, queues a batch or duplicates a template — all of which spend credits. A 504 from the gateway does not mean the generation didn't happen, so repeating it can bill you twice. If you retry a write yourself, verify first: poll the generation request, or list what exists.
Why a bare 429 is retried exactly once
On this API 429 means three different things, and two of them share an id.
request_rate_limited is a genuine endpoint throttle. It carries Retry-After, so the SDK waits exactly that long and tries again — nothing to guess.
feature_not_in_plan means your plan excludes that design type. Unambiguous, and permanent until the plan changes, so it is never retried.
rate_limit_exceeded is the hard one: it answers both a spent credit balance, which no amount of waiting fixes, and the global 10 requests/second ceiling, which clears in under a second. Only message distinguishes them, and the ceiling is enforced in front of the API, so its refusal carries neither Retry-After nor reliably the error envelope at all.
So the SDK does not try to classify it. It retries once, after a fixed second — the ceiling is per-second, so one second is what clearing it takes. Guessing wrong costs one second on a call that was already failing; not guessing at all costs the whole call, and generation endpoints have no endpoint budget, which makes the ceiling the only limit a burst of parallel generation calls can hit.
Set ABYSSALE_MAX_RETRIES=0 to switch this off with everything else. Note that a refused request still counts against your budget. Full taxonomy on Rate limits.
Requests are aborted after ABYSSALE_TIMEOUT_MS (default 30 s). Any AbortSignal you attach to a request is honoured alongside the timeout, so your own cancellation still works.
Polling helpers
waitForGenerationRequest and waitForDuplicationRequest run the polling loop for you — call and await, instead of writing the backoff yourself.
const { data: request, error } = await abyssale.generateMultiFormatMedia(designId, {
elements: { text_title: { payload: 'New product launch' } },
});
if (error || !request.generation_request_id) throw new Error('Generation did not start');
// Polls with exponential backoff until is_finalized: true
const result = await abyssale.waitForGenerationRequest(request.generation_request_id);
for (const banner of result.banners) {
console.log(banner.format?.id, banner.file.cdn_url);
}Both accept the same options object. Values below the minimum are raised to it, not honoured:
| Option | Default | Minimum | Description |
|---|---|---|---|
intervalMs | 3000 | 2000 | Delay before the first re-check. Doubles after each poll, up to maxIntervalMs. |
maxIntervalMs | 30000 | 5000 | Ceiling for the backoff interval. |
timeoutMs | 1800000 (30 min) | 60000 | Total budget. Throws once the next wait would exceed it. |
A poll that fails is retried only when retrying could help — the same rule the HTTP layer uses. A 5xx, a dropped connection, or a 429 carrying Retry-After is treated as a blip and absorbed, up to three in a row; when the server sends Retry-After, that wait is used instead of the backoff interval. A bare 429 is probed once, for the reason above — but only once per wait, not once in a row: a second one answers the question, so a spent credit balance ends the wait rather than being re-asked for half an hour. Anything else is a verdict and ends the wait immediately, so a generation_request_not_found or a feature_not_in_plan throws on the first poll.
const result = await abyssale.waitForGenerationRequest(id, {
intervalMs: 5_000,
timeoutMs: 300_000,
});Unlike the request methods, these helpers do throw — always an AbyssalePollingError, never a raw SyntaxError or bare Error:
| Property | Contents |
|---|---|
id | The API's error code, when the failure carried one — branch on this. |
response | The parsed API error envelope. |
cause | The original error value, untouched. |
import abyssale, { AbyssalePollingError } from '@abyssale/sdk';
try {
const result = await abyssale.waitForGenerationRequest(id);
} catch (err) {
if (err instanceof AbyssalePollingError && err.id === 'generation_request_gone') {
// The request expired — generation requests are kept 7 days
} else throw err;
}A timeout is not a failed generation: the message reads no result after 1800s — the request may still complete. For batches that outlive your process, prefer a callback_url and a webhook over polling.
Duplication resolves on failure too
waitForDuplicationRequest resolves when the request reaches COMPLETED or ERROR — reaching a terminal state is not the same as succeeding. Always check result.status before using result.designs.
TypeScript
Every request and response type is generated from the OpenAPI spec, so element names and output types complete as you type. The public types are exported from the package root:
import type {
Banner, Design, DesignElement, DesignFormat, DesignAnimation, ErrorResponse,
Elements, AsyncElements, Pages, Font, ProjectSummary, GenerationRequestStatus,
DynamicImageResponse, DuplicationRequest, DuplicationRequestStatus, DuplicatedDesign,
WorkspaceTemplate, WorkspaceTemplateCategory, PollOptions,
} from '@abyssale/sdk';components is exported too, for reaching a schema that has no named alias: components['schemas']['DesignImportProblem'].
What the SDK does not cover
- Design Import — the import surface is in Alpha and its schema can still change, so it is deliberately excluded rather than shipped and broken. Call
POST /designs/import/jsondirectly until it stabilises. - Webhook receiving — webhooks arrive at your server; there is nothing for a client to call. Payloads are on Events.
Related
- Quickstart — first image in four calls, SDK included
- Errors — every
idtheerrorobject can carry - Rate limits — the budgets behind the retry rules
- Authentication — where
ABYSSALE_API_KEYcomes from - npm package · GitHub repository
