Skip to content
Programmabledigital
AuthenticationEndpointsWebhooksOpenAPI

API docs

Version 1.2.0

Read what you make in Programmable, start animations from a prompt, render videos, and hear about it all with signed webhooks. Every request goes to https://programmable.digital/api/integrations/v1.

The machine-readable description is at /api/integrations/v1/openapi.json (OpenAPI 3.1). Components from the marketplace install with npx programmable-cli add <component>.

Authentication

Create a key in the studio under Developers › API keys and choose what it may do. Send it with every request:

curl https://programmable.digital/api/integrations/v1/me \
  -H "Authorization: Bearer pgm_…"

A key is shown once, when it is created. Revoke it any time; it stops working at once. A request without the scope an endpoint needs is answered with 403.

Scopes

components:readList published animations and their embeds (Shopify, WordPress, Webflow).
animations:readList and read your animations, with share, embed and poster links.
decks:readList and read your decks, with share links.
graphics:readList and read your graphics.
logos:readList and read your logos.
leads:readList the emails people left on your sign-up cards.
animations:writeStart new animations from a prompt. Uses your credits like the studio.Starts work; credits are used exactly as in the studio.
animations:shareMake an animation's public share link, or turn it off.Makes things public; sharing is free and needs no credits.
exports:writeRender MP4 or GIF exports of your animations.Starts work; credits are used exactly as in the studio.

Rate limits

300 requests a minute from one address and 120 a minute per key. Over the limit, the answer is 429 with Retry-After: 60. Starting animations and renders has its own, lower limits, as in the studio.

Errors

Errors are JSON: { "error": { "code": "not_found", "message": "No such animation." } }. Lists take limit (1 to 100) and offset, and answer with data, total and next_offset.

Endpoints

  • GET /me Check a key
  • GET /components List published components, newest first
  • GET /components/{token} One published component of this account
  • GET /poster/{token} Poster frame (public, no key)
  • GET /marketplace The component marketplace catalog, with this month's download allowance
  • GET /marketplace/{slug} Download a marketplace component's source (counts toward the monthly allowance)
  • GET /animations List your animations
  • POST /animations Start an animation from a prompt
  • GET /animations/{id} Get one of your animations
  • POST /animations/{id}/share Publish or turn off an animation's share link
  • GET /decks List your decks
  • GET /decks/{id} Get one of your decks
  • GET /graphics List your graphics
  • GET /graphics/{id} Get one of your graphics
  • GET /logos List your logos
  • GET /logos/{id} Get one of your logos
  • GET /jobs/{id} Get a generation job
  • GET /leads List leads
  • POST /exports Render an MP4 or GIF
  • GET /exports/{id} Get an export
  • GET /exports/{id}/download Download a finished export
  • GET /openapi.json This document

GET /me

Check a key.

Scope: components:read

Answers 200: version, key.

GET /components

List published components, newest first.

Scope: components:read

q querystringCase-insensitive title filter.
limit queryinteger
offset queryinteger

Answers 200: version, components, total, nextOffset.

GET /components/{token}

One published component of this account.

Scope: components:read

token pathstring

Answers 200: version, component.

GET /poster/{token}

Poster frame (public, no key).

Scope: components:read

No key needed.

token pathstring
v queryintegerVersion pin from posterUrl (longer cache).

Answers 200.

GET /marketplace

The component marketplace catalog, with this month's download allowance.

q querystringCase-insensitive name/slug/description filter.
category querystringOne of the marketplace categories (charts, dashboards, pricing, product-demos, ...).

Answers 200: version, components, total, allowance.

GET /marketplace/{slug}

Download a marketplace component's source (counts toward the monthly allowance).

slug pathstring
format query"json" | "tsx"json: metadata plus the file in `source`; tsx: the .tsx file itself as text/plain.

Answers 200: any.

GET /animations

List your animations.

Scope: animations:read

limit queryinteger
offset queryinteger
q querystringCase-insensitive title filter.

Answers 200: object, data, total, next_offset.

POST /animations

Start an animation from a prompt. Starts a new animation the way the studio's chat does: research, storyboard, animate and review. It runs for several minutes; poll GET /jobs/{id} or open it in the studio. Credits are charged as in the studio and refunded if nothing is saved. With share: true (the key also needs animations:share) the share link is published when it finishes, by the same rules as POST /animations/{id}/share: the job then has share_url, or share_error if it couldn't be shared (the animation is still saved).

Scope: animations:write

Body

promptrequiredstringWhat the animation should show and say.
titlestring
duration_secondsinteger
aspect"landscape" | "square" | "portrait" | "wide" | "tall" | "feed" | "video"
company_namestring
draftbooleanStop at the first checked cut (faster, fewer credits).
sharebooleanPublish the share link when it finishes. Needs the animations:share scope as well (403 without it); not allowed with draft. Sharing is free.

Answers 202: job.

GET /animations/{id}

Get one of your animations.

Scope: animations:read

id pathstring (uuid)

Answers 200: Animation.

POST /animations/{id}/share

Publish or turn off an animation's share link. What the studio's Share button does. { enabled: true } publishes the public share link (/share/<token>) and answers the Animation with share_url, embed_script_url and poster_url; { enabled: false } turns it off, and the old link stops working. Idempotent: sharing an animation that is already shared returns the same link (it is never rotated), and turning off one that isn't shared answers 200 with share_url null. Only your own animations, never one in Trash (404). In a workspace that needs approval, an unapproved animation answers 409 APPROVAL_REQUIRED and nothing is shared. Sharing is free, is written in the workspace activity log (marked via api) and sends the share.created webhook.

Scope: animations:share

id pathstring (uuid)

Body

enabledrequiredbooleantrue to publish the link, false to turn it off.
expected_versionintegerThe animation's version you read; when it has changed since, 409 VERSION_CONFLICT. Without it, the current version is used.

Answers 200: Animation.

GET /decks

List your decks.

Scope: decks:read

limit queryinteger
offset queryinteger
q querystringCase-insensitive title filter.

Answers 200: object, data, total, next_offset.

GET /decks/{id}

Get one of your decks.

Scope: decks:read

id pathstring (uuid)

Answers 200: Item.

GET /graphics

List your graphics.

Scope: graphics:read

limit queryinteger
offset queryinteger
q querystringCase-insensitive title filter.

Answers 200: object, data, total, next_offset.

GET /graphics/{id}

Get one of your graphics.

Scope: graphics:read

id pathstring (uuid)

Answers 200: Item.

GET /logos

List your logos.

Scope: logos:read

limit queryinteger
offset queryinteger
q querystringCase-insensitive title filter.

Answers 200: object, data, total, next_offset.

GET /logos/{id}

Get one of your logos.

Scope: logos:read

id pathstring (uuid)

Answers 200: Item.

GET /jobs/{id}

Get a generation job.

Scope: animations:write or animations:read

id pathstring (uuid)

Answers 200: job.

GET /leads

List leads. Emails people left on your animations' sign-up cards, newest first.

Scope: leads:read

limit queryinteger
offset queryinteger
animation_id querystring (uuid)Only this animation's leads.

Answers 200: object, data, total, next_offset.

POST /exports

Render an MP4 or GIF. Renders in the background. Poll GET /exports/{id}, or listen for the render.completed webhook. The file can be downloaded for a day.

Scope: exports:write

Body

animation_idrequiredstring (uuid)
format"mp4" | "gif"

Answers 202: export.

GET /exports/{id}

Get an export.

Scope: exports:write

id pathstring (uuid)

Answers 200: export.

GET /exports/{id}/download

Download a finished export.

Scope: exports:write

id pathstring (uuid)

Answers 200.

GET /openapi.json

This document.

No key needed.

Answers 200: .

Objects

Animation

object"animation"
idstring (uuid)
titlestring
versioninteger
aspectstring
widthinteger
heightinteger
duration_secondsinteger
created_atstring (date-time), or null
updated_atstring (date-time), or null
share_urlstring (uri), or nullThe public share link (also the iframe embed address), or null when not shared.
embed_script_urlstring (uri), or null
poster_urlstring (uri), or nullA still frame (PNG), or null when not shared.
studio_urlstring (uri)

Item

A deck, graphic or logo.

object"deck" | "graphic" | "logo"
idstring (uuid)
titlestring
versioninteger
created_atstring (date-time), or null
updated_atstring (date-time), or null
share_urlstring (uri), or null
studio_urlstring (uri)

Lead

object"lead"
idstring (uuid)
emailstring (email)
namestring, or null
page_urlstring (uri), or null
source"embed" | "share"
animation_idstring (uuid)
variant"a" | "b"
created_atstring (date-time)

Job

object"job"
idstring (uuid)
type"animation.generate"
status"running" | "succeeded" | "failed" | "cancelled"
titlestring
messagestring, or nullThe latest progress message.
errorstring, or null
animation_idstring (uuid), or null
animation_urlstring (uri), or null
share_urlstring (uri), or nullWith share: true, the published share link (/share/<token>) once it succeeded.
share_error"APPROVAL_REQUIRED" | "SHARE_FAILED"With share: true, why the finished animation wasn't shared (it is still saved). APPROVAL_REQUIRED: approve it in the studio, then share it with POST /animations/{id}/share.
share_error_messagestring, or nullThe reason in words, for share_error.
created_atstring (date-time)
updated_atstring (date-time)

Export

object"export"
idstring (uuid)
status"queued" | "running" | "succeeded" | "failed"
errorstring, or null
download_urlstring (uri), or nullWith the same API key, for a day after it finishes.

Webhooks

Add endpoints in the studio under Developers › Webhooks, each with the events it should receive and its own signing secret. Each event is one POST with a JSON body and these headers:

  • Programmable-Event: the event name.
  • Programmable-Delivery: the event id. The same event is never sent twice on purpose, but a retry can arrive after a timeout: ignore an id you have already handled.
  • Programmable-Signature: t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of t, a dot and the raw body, keyed with the endpoint’s secret.

Answer with any 2xx within 10 seconds. Anything else is retried after 1, 5 and 30 minutes and 2 hours, then marked failed in the endpoint’s log, where you can retry it.

Checking the signature

import { createHmac, timingSafeEqual } from "node:crypto";

// Express: app.post("/webhooks/programmable", express.raw({ type: "application/json" }), handler)
export function verifyProgrammable(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  if (!/^[a-f0-9]{64}$/.test(parts.v1 || "")) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}

// verifyProgrammable(req.body.toString("utf8"), req.get("Programmable-Signature"), process.env.PROGRAMMABLE_WEBHOOK_SECRET)
import hmac, hashlib, time

def verify_programmable(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Check the signature against the raw bytes you received, before parsing the JSON, and reject timestamps more than 5 minutes old.

Events

lead.created

Someone left their email on an animation that collects emails.

{
  "type": "lead.created",
  "id": "4f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
  "created_at": "2026-10-27T09:30:00.000Z",
  "lead": {
    "email": "[email protected]",
    "name": "Jane Example",
    "page_url": "https://example.com/pricing",
    "source": "embed"
  },
  "animation": {
    "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
    "title": "Spring launch"
  }
}

render.completed

An MP4 or GIF finished rendering and can be downloaded.

{
  "type": "render.completed",
  "id": "evt_3f8a1c0b9d2e4f6a8b7c",
  "created_at": "2026-10-27T09:30:00.000Z",
  "data": {
    "item": {
      "kind": "animation",
      "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
      "title": "Spring launch"
    },
    "format": "mp4",
    "job_id": "3a0f6c1e-5b2d-4e8f-9c7a-1d2e3f4a5b6c",
    "download_url": "https://programmable.digital/api/studio?action=render-download&id=3a0f6c1e-5b2d-4e8f-9c7a-1d2e3f4a5b6c",
    "expires_at": "2026-10-28T12:00:00.000Z"
  }
}

export.completed

A background export (a platform pack or a sent file) finished.

{
  "type": "export.completed",
  "id": "evt_3f8a1c0b9d2e4f6a8b7c",
  "created_at": "2026-10-27T09:30:00.000Z",
  "data": {
    "item": {
      "kind": "animation",
      "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
      "title": "Spring launch"
    },
    "format": "pack",
    "job_id": "3a0f6c1e-5b2d-4e8f-9c7a-1d2e3f4a5b6c",
    "filename": "spring-launch-social.zip",
    "download_url": "https://programmable.digital/api/studio?action=render-download&id=3a0f6c1e-5b2d-4e8f-9c7a-1d2e3f4a5b6c",
    "expires_at": "2026-10-28T12:00:00.000Z"
  }
}

item.published

Something was posted to a social account or placed on a site.

{
  "type": "item.published",
  "id": "evt_3f8a1c0b9d2e4f6a8b7c",
  "created_at": "2026-10-27T09:30:00.000Z",
  "data": {
    "item": {
      "kind": "animation",
      "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
      "title": "Spring launch"
    },
    "service": "linkedin",
    "location": "LinkedIn (posted)",
    "link": "https://www.linkedin.com/feed/update/urn:li:share:7000000000000000000",
    "scheduled": false
  }
}

version.created

An animation, deck, graphic or logo was saved as a new version.

{
  "type": "version.created",
  "id": "evt_3f8a1c0b9d2e4f6a8b7c",
  "created_at": "2026-10-27T09:30:00.000Z",
  "data": {
    "item": {
      "kind": "animation",
      "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
      "title": "Spring launch"
    },
    "version": 4,
    "label": "Edited"
  }
}

share.created

A share link was turned on for an animation, deck, graphic or logo.

{
  "type": "share.created",
  "id": "evt_3f8a1c0b9d2e4f6a8b7c",
  "created_at": "2026-10-27T09:30:00.000Z",
  "data": {
    "item": {
      "kind": "animation",
      "id": "6f1c2a9e-0d3b-4c51-9a7e-2b8d4f6e1a05",
      "title": "Spring launch"
    },
    "share_url": "https://programmable.digital/share/0123456789abcdef0123456789abcdef0123456789abcdef"
  }
}
© 2026 Programmable
HomePricingComponentsLegal