5 min read

Animate a photo via API: integration tutorial

Step-by-step tutorial to integrate the Incarn photo animation API: create a key, start an animation, get the result via a signed webhook, handle credits and errors. curl and Node.js examples.

APIdeveloperstutorialintegrationwebhook
Thomas Moreau
Thomas Moreau

AI & Technology Writer, Incarn

In short

Integrating old-photo animation into your app takes four steps: create a key, send the photo to POST /v1/animations (file or URL), get the video by polling or via a signed HMAC-SHA256 webhook, then handle credits (402) and idempotency. curl and Node.js examples, from the first call to the production webhook, below.

You want an old photo to move, from your code, without building a video pipeline. This tutorial takes you from the first curl call to a production webhook, with the Incarn API.

The idea fits in one sentence: you send a photo, you get a short, natural video back. The rest (validation, model choice, HD) is handled server-side. Budget ten minutes for a first render.

What you'll build

A function that takes an old photo and returns the URL of an animated video. Two ways to get the result: polling (simple, to start) or a webhook (clean, for production). We'll cover both.

Step 1: create your API key

Head to your developer dashboard and create a key. It's shown only once, so store it server-side (an environment variable), never in a browser or a public repo.

export INCARN_KEY="ik_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Authentication is a Bearer token on every request:

Authorization: Bearer ik_live_xxx

Step 2: start an animation

The endpoint is POST /v1/animations. You send the photo either as a file in multipart/form-data, or as a public URL in JSON.

In curl, with a URL:

curl -X POST https://api.incarn.co/v1/animations \
  -H "Authorization: Bearer $INCARN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image_url":"https://your-app.com/grandmother.jpg","prompt":"slight smile"}'

In Node.js, the same thing:

const res = await fetch("https://api.incarn.co/v1/animations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.INCARN_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    image_url: "https://your-app.com/grandmother.jpg",
    prompt: "slight smile",
    webhook_url: "https://your-app.com/incarn", // optional, see step 3
  }),
});

const job = await res.json();
// { id: "clx...", status: "processing", video_url: null, ... }

Processing is asynchronous: the response comes back right away with an id and a processing status. The video isn't ready yet.

One tip already: add an Idempotency-Key header. If your request goes out twice (network retry, double click), the same key returns the original animation without starting a new generation or spending a credit again.

headers: {
  Authorization: `Bearer ${process.env.INCARN_KEY}`,
  "Content-Type": "application/json",
  "Idempotency-Key": `photo-${photoId}`,
}

Step 3: get the result

Two options. Pick one based on your architecture.

Option A: polling

You poll GET /v1/animations/:id until the status turns to succeeded or failed. Allow 1 to 3 minutes, poll every few seconds.

async function waitForVideo(id) {
  while (true) {
    const res = await fetch(`https://api.incarn.co/v1/animations/${id}`, {
      headers: { Authorization: `Bearer ${process.env.INCARN_KEY}` },
    });
    const job = await res.json();
    if (job.status === "succeeded") return job.video_url;
    if (job.status === "failed") throw new Error(job.error);
    await new Promise((r) => setTimeout(r, 3000));
  }
}

Simple, perfect for a script or a prototype. In production, prefer the webhook: no loop to maintain, no connection held open for minutes.

Option B: the signed webhook

Pass a webhook_url at creation (step 2). As soon as the job finishes, Incarn sends a POST to that URL with the same body as GET /v1/animations/:id.

Each delivery is signed. The X-Incarn-Signature header is sha256=<hmac>, an HMAC-SHA256 of the raw body with your signing secret (shown in the dashboard). Verify it before processing the event, otherwise anyone could post fake results.

import { createHmac, timingSafeEqual } from "crypto";

app.post("/incarn", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.header("X-Incarn-Signature") || "";
  const expected =
    "sha256=" +
    createHmac("sha256", process.env.INCARN_WEBHOOK_SECRET)
      .update(req.body) // the RAW body, not the parsed JSON
      .digest("hex");

  const ok =
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const job = JSON.parse(req.body.toString());
  if (job.status === "succeeded") {
    // job.video_url points to the HD MP4: store it, notify the user
  }
  res.status(200).end(); // acknowledge quickly
});

Two details that avoid bugs: sign over the raw body (not the already-parsed object, re-serialization would change the signature), and dedupe on receipt using job.id, a delivery can be retried.

Step 4: handle real-world cases

The happy path is simple. Here are the few cases to cover for a clean integration.

  • Out of credits (402). The model is prepaid: when you run out, the API returns 402 and starts nothing. Top up from the dashboard, or enable auto-recharge (buying a capped pack when the balance drops below a threshold) so your queue never stalls.
  • Too many requests or parallel generations (429). Creation is limited to 10 requests/minute, and the number of simultaneous generations depends on your plan (2, 5 or 15). On a 429, retry when a generation finishes.
  • Generation failure. The status turns to failed, error explains, and the credit is automatically refunded. Show a clear message and, if useful, offer a retry.
  • Safe retries. The Idempotency-Key from step 2 makes every retry harmless: never a double generation, never a double charge.

Going further

You have the skeleton of a full integration. The rest is your product: a genealogy app that animates the portraits in a family tree, a memorial service that gives motion back to a photo, a greeting card that moves.

The full documentation details every field, error code and the signature-verification example. If you're still weighing the options on the market, our comparison of APIs to animate an old photo places Incarn against the generic primitives. And to create your key, it all starts on the developer page.

One photo, one call, one video. We handle the rest.

Thomas Moreau
Thomas Moreau

AI & Technology Writer, Incarn

Thomas covers AI and machine learning applications for creative tools. Former research engineer with a focus on computer vision and video generation.

Ready to try it yourself?

Animate your first photo for free, in just a few moments.

Try Incarn free →

Read next