NetraFlow

Webhooks

Receive real-time notifications when jobs complete, fail, or are cancelled.

Webhooks deliver HTTP POST notifications to your server when a job or batch changes state. Available on Starter, Pro, and Scale — a webhook_url on a Free-plan request is rejected with MODE_NOT_ALLOWED (403).

Events

PropTypeDescription
job.completedterminalJob finished successfully. Results are available via GET /v1/jobs/:id.
job.failedterminalJob failed at any pipeline stage. Failed jobs are not charged.
job.cancelledterminalA job that had already started processing was cancelled. Cancelling a job that is still pending does not emit this event, and neither does a held job expiring — poll GET /v1/jobs/:id to confirm those.
job.heldnon-terminalThe job passed the submit check, but its real cost exceeds your balance. It parks until you add credits, then resumes automatically. Carries credits_needed and balance.
batch.completedterminalEvery job in the batch reached a terminal state. Carries per-status counts and aggregate results.
batch.pausednon-terminalNo runnable jobs remain and at least one is held. Carries credits_needed_to_resume.

Payload

{
  "event": "job.completed",
  "job_id": "job_abc123",
  "status": "completed",
  "completed_at": "2026-04-03T12:00:45.000Z",
  "credits_charged": 5
}
{
  "event": "job.held",
  "job_id": "job_abc123",
  "status": "held",
  "credits_charged": null,
  "reason": "insufficient_credits",
  "credits_needed": 26,
  "balance": 4
}

Held is not terminal, so there is no completed_at.

{
  "event": "batch.paused",
  "batch_id": "bat_abc123",
  "status": "paused",
  "rollup": { "completed": 4, "failed": 0, "held": 6, "pending": 0 },
  "credits_needed_to_resume": 48
}

batch.completed carries total_jobs, completed_count, failed_count, credits_charged, aggregate_results, and completed_at.

PropTypeDescription
eventstringOne of the six event types listed above.
job_idstringJob identifier. Present on job.* events.
batch_idstring (optional)Batch identifier. Present on batch.* events, and on job.* events for jobs that belong to a batch.
statusstringThe status the job or batch moved into.
completed_atstring (ISO 8601, optional)When a terminal state was reached. Absent on job.held and batch.paused.
credits_chargedinteger | nullCredits consumed. Always present on job events, but null when nothing was billed — job.failed, job.held, and cancels that spent no metered compute.

credits_charged is nullable, and events other than the three job-terminal ones are real. A consumer that types event as a closed three-value union, or credits_charged as a non-null integer, will reject valid live deliveries.

Setting up webhooks

Pass a webhook_url when you create a job or a batch. The URL is per-request — there is no account-level endpoint to register.

curl -X POST https://api.netraflow.com/v1/jobs \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: sk_live_your_key_here" \
  -d '{
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "capabilities": ["transcription"],
    "webhook_url": "https://your-server.com/webhooks/netraflow"
  }'

Return 200 OK immediately before processing the payload. The webhook timeout is 5 seconds — if your server does heavy processing synchronously, you risk unnecessary retries. Acknowledge first, process async.

Your webhook secret

Every workspace has one signing secret, prefixed whsec_. It is created with the workspace — there is nothing to enable.

  • Dashboard — Settings → Webhook secret → Reveal secret. The same card rotates it.
  • APIGET /v1/account returns it as webhook_secret.

Rotating takes effect immediately and invalidates the previous secret, so deploy the new value to your receiver before rotating.

Signature verification

Webhook requests include an X-NetraFlow-Signature header containing an HMAC-SHA256 signature of the raw JSON body, signed with your webhook secret.

import crypto from 'node:crypto';

function verifyWebhook(body, signature, secret) {
  if (typeof signature !== 'string') return false;
  const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
  const a = Buffer.from(signature, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // Length check first: timingSafeEqual throws on a length mismatch, which would
  // turn a malformed request into a 500 instead of the 401 we want.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// In your request handler:
const body = await request.text();
const signature = request.headers.get('X-NetraFlow-Signature');

if (!verifyWebhook(body, signature, WEBHOOK_SECRET)) {
  return new Response('Invalid signature', { status: 401 });
}

// Signature valid — process the event
const event = JSON.parse(body);

Retry strategy

Failed deliveries are retried with exponential backoff:

PropTypeDescription
Attempt 1ImmediateFirst delivery attempt, no delay.
Attempt 21 secondFirst retry after 1 second.
Attempt 310 secondsSecond retry after 10 seconds.
Attempt 460 secondsFinal retry after 60 seconds.

Each attempt has a 5-second timeout. A delivery is successful when your server returns any 2xx status code.

Requirements

  • HTTPS only — HTTP URLs are rejected at job creation time.
  • Private and reserved addresses blocked — URLs resolving to loopback, private, link-local, or cloud-metadata addresses are rejected to prevent SSRF. IPv6 literal hosts are rejected outright; use a domain name.

On this page