Nubu
  • Features
  • How it works
  • Pricing
  • Integrations
  • FAQ
  • Contact
  • Docs
  • Blog
  • Log in
  • Log inSign up
    Nubu

    Creative automation for teams that ship campaigns, not busywork.

    Try for free
    Product
    FeaturesNubu MotionHow it worksPricingIntegrationsFAQ
    Company
    AboutContactDocsBlogLog in
    Legal
    Privacy policyTerms of serviceData deletion
    © 2026 Bear Studios · Nubu
    Introduction
    Templates overviewInstalling Nubu BuilderAdding template compsAdding propertiesAdding footageSetting defaultsExporting your templateUploading a template
    Flows overviewAvailable nodesAdding a nodeConnecting nodesWorking with dataTemplates and outputsUsing AI nodesBuilding a creative
    CampaignsTemplatesAssetsGlossariesRendersSettings
    Connecting Meta AdsConnecting Google AdsConnecting OpenAIConnecting Gemini AI StudioConnecting Anthropic
    AI Assistant overviewPrompt examplesAssistant settings and controls
    Nubu Motion scripts: the reference
    OverviewQuick startDifferences from After EffectsDeterminism
    JavaScript supportReferences and idsProject scriptsState and timeBudgets and errorsFormatting values
    GlobalTime conversionInterpolationVector mathsRandom and noiseColour conversionOther maths
    LayerCompPropertyKeyframePathGeometryProjectComponentConsoleComponentControlExpressionControl
    The property vocabularyAnimators propertiesAudio propertiesContent propertiesContents propertiesEffects propertiesExpression controls propertiesFills propertiesInstance controls propertiesLayer propertiesLayout propertiesMasks propertiesStrokes propertiesText style propertiesRig propertiesRigging propertiesRig controllers propertiesRigging weights propertiesRigging mesh propertiesRig items propertiesRig splines propertiesRig simulation propertiesRig properties propertiesRig drivers propertiesRig draw order propertiesTransform propertiesTrim path properties
    Error codesBudgets
    Changelog
    API overviewAuthentication and keysRate limits and errorsUploading assetsFoldersUploading templatesTriggering a flowRuns and creativesAPI Request deliveryCallbacks
    All docs
    API

    Callbacks

    Let Nubu tell your software when a run changes state instead of polling.

    Pass a callback_url when you trigger a flow and Nubu POSTs to it every time the run changes state: creatives built, approved, queued, rendering, rendered or failed, and each delivery outcome. Polling GET /runs/<run_id> always works too; callbacks save you the polling.

    The request#

    {
      "event": "run.updated",
      "event_id": "evt_2a4f6c1e-7d1b-4d64-9c2e-8d0a2f3b4c5d",
      "run_id": "...",
      "run_version": 17,
      "status": "completed",
      "external_id": "customer-job-48291",
      "changes": {
        "status": true,
        "creatives": [{ "creative_id": "...", "from": "rendering|-", "to": "rendered|uploaded" }]
      },
      "run": { ... },
      "sent_at": "..."
    }
    
    • event_id names this event. A retry re-sends the same event_id with the same body, so de-duplicate on it.
    • run_version is the run's version after this change: a counter that only ever goes up, incremented on every state change Nubu records for the run. It is the ordering rule (below).
    • status is the run's state after the change (Runs and creatives).
    • changes says what moved: whether the run's status changed, and each creative whose status or delivery state changed (from and to read status|delivery, with - for no delivery).
    • run is the full run as GET /runs/<run_id> would answer it at that moment, creatives included.

    The request carries Content-Type: application/json, User-Agent: Nubu-Callbacks/1.0, X-Nubu-Event: run.updated, X-Nubu-Run-Id and the three signature headers below.

    Verifying a callback#

    Every callback is signed, so your endpoint can prove it came from Nubu and that the body was not changed on the way. Three headers carry the proof:

    HeaderValue
    X-Nubu-Event-Idevt_..., the same as the body's event_id, stable across retries
    X-Nubu-TimestampUnix seconds when this attempt was signed
    X-Nubu-Signaturev1=<hex>: HMAC-SHA256, keyed with your signing secret, over the exact string event_id + "." + timestamp + "." + raw_request_body

    The signed string is the event id, a dot, the timestamp, a dot and the raw request body exactly as received (the bytes, not a re-serialised JSON). The signing secret is the organisation's, created with its first API key and shown once; the owner can rotate it in Settings > Integrations > API > Signing secret (rotation replaces it at once). API Request deliveries carry the same three headers, signed with the same secret (API Request delivery).

    import { createHmac, timingSafeEqual } from "node:crypto";
    
    function verifyNubu(req, rawBody, secret) {
      const eventId = req.headers["x-nubu-event-id"];
      const timestamp = req.headers["x-nubu-timestamp"];
      const signature = req.headers["x-nubu-signature"]; // "v1=<hex>"
      if (!eventId || !timestamp || !signature?.startsWith("v1=")) return false;
      // Refuse stale attempts: five minutes is a comfortable window.
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
      const expected = createHmac("sha256", secret)
        .update(`${eventId}.${timestamp}.${rawBody}`)
        .digest("hex");
      const given = signature.slice(3);
      return given.length === expected.length && timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
    }
    

    Read the raw body before any JSON parsing (most frameworks offer a raw-body option), compare with a constant-time comparison, and reject anything that fails: a wrong secret, an edited body or a replay outside your window. Then de-duplicate on event_id and order by run_version.

    Answering#

    Answer with any 2xx status as quickly as you can and do your work afterwards. Nubu waits ten seconds for an answer, then makes up to three attempts (immediately, after two seconds, after eight more), every attempt carrying the same event_id and the same body. Redirects are never followed: a 3xx answer counts as a failure. After the third failure that change is not retried; the next change sends the current state again, and the run is always readable with GET /runs/<run_id>. The last attempt's status and error are recorded on the run.

    Rules of thumb#

    • Use https on a public host; a private or local address is refused when the flow is triggered, and a redirect to one is never followed.
    • Verify the signature before acting on a callback, and de-duplicate on event_id: a retry re-sends the same event with the same body.
    • Callbacks can arrive out of order under retries. Order them by run_version and ignore any callback whose run_version is lower than the last one you applied for that run: if 17 arrives and then 16, 16 is stale. sent_at is metadata about when the attempt was made, never the consistency rule. GET /runs/<run_id> carries the current run_version too.
    • Treat a callback as a signal, not as the only record: run carries everything the API would answer, so you can act on it directly or read the run again.
    • Keep external_id in your request and you never need to map run ids yourself.