Janvia

API reference

Callbacks

When a task ends we POST it to your URL, signed so you can verify it came from us, and retry until you take it.

Pass callback_url (a public https URL) when you submit a task. When the task completes or fails, we POST the task object to it, the same as GET /v1/tasks/TASK_ID returns.

The request carries these headers:

Content-Type: application/json
X-Relay-Task-Id: task_9f2c...
X-Relay-Attempt: 1
X-Relay-Signature: 9b1d0c...

Retries

Answer with a 2xx to accept it. Any other status, no answer within 10 seconds, or a redirect (we don't follow them) counts as a failure, and we try again later: up to 6 attempts in all, after 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours. X-Relay-Attempt is the attempt number.

The same task may reach you more than once, for example when your answer got lost on the way back. Use the task ID to handle each task once.

Delivery status

The task's callback field shows how delivery is going:

"callback": {
  "url": "https://example.com/hooks/relay",
  "status": "delivered",
  "attempts": 1,
  "last_attempt_at": "2026-10-21T08:00:13.000Z",
  "last_error": null
}
  • status: waiting (the task is still running), pending (being sent, or waiting to retry), delivered, or failed (every attempt failed).
  • last_error: why the last attempt failed, such as HTTP 500 or no response within 10s.

The task detail in the console shows the same.

Sending it again

To get a finished task's callback again, after fixing your endpoint for example, click Send again in the console's task detail, or call:

curl -X POST https://janvia.io/v1/tasks/TASK_ID/callback \
  -H "Authorization: Bearer $API_KEY"

This starts a fresh round of attempts and answers 202 with the task. A task that is still running or has no callback_url returns 400.

Callbacks are a convenience. If one never arrives, fetch the task with GET /v1/tasks/TASK_ID. Tasks run from the playground have no callback.

Verifying the signature

X-Relay-Signature is the hex HMAC-SHA256 of the raw request body. The secret is the hex SHA-256 of the API key that submitted the task. Compute it over the bytes you received; don't parse and re-serialize the JSON first. If that key has since been deleted, the request carries no signature.

Node.js:

import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, signature, apiKey) {
  const secret = createHash('sha256').update(apiKey).digest('hex');
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  return (
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  );
}

Python:

import hashlib, hmac

def verify(raw_body: bytes, signature: str, api_key: str) -> bool:
    secret = hashlib.sha256(api_key.encode()).hexdigest().encode()
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)