Webhook integration

Webhooks are optional. The baseline integration flow is polling job status until the result is ready.

This guide describes how to initiate a retouching job via the API and how to handle asynchronous webhook notifications securely.

Recommended baseline: use /retoucher/status/{id} polling as the default integration approach.
Optional extension: use webhooks only if you want completion callbacks pushed to your server.

Before you use webhooks

Webhooks are optional.

Before using them, send the list of domains that will receive webhook requests to relu@retouch4.me.

Webhook delivery works only after those domains are approved and added to the server webhook whitelist.

If you do not need callback delivery, skip the hook field and use status polling as your baseline flow.

Initiating a job

To start a new image processing task, send a multipart/form-data POST request to the API.

Upon a successful request, one retouch credit will be deducted from your account balance, and the task will be added to the processing queue.

Endpoint:
POST https://retoucher.hz.labs.retouch4.me/api/v1/retoucher/start

Headers

Header Required Description
X-Retouch-Token Yes Your API authentication token.
Idempotency-Key Optional A unique string such as a UUID to prevent duplicate processing if the request is retried.

Form data parameters

Parameter Type Required Description
file File Yes The source image file (.png or .jpg).
payload String (JSON) Yes JSON string containing processing configurations (mode and plugins).
hook String (URL) Optional The callback URL where the webhook will be sent upon completion. Omit this field if you use polling only.

Request example

curl --location --request POST 'https://retoucher.hz.labs.retouch4.me/api/v1/retoucher/start' \
  --header 'X-Retouch-Token: <your_token_here>' \
  --header 'Idempotency-Key: <unique_random_string>' \
  --form 'file=@"/path/to/image.jpg"' \
  --form 'payload="{\"mode\":\"professional\",\"tasks\":[{\"Plugin\":\"Clean Backdrop\",\"Scale\":0,\"Layer\":1,\"Alpha1\":1}]}"' \
  --form 'hook="https://yourdomain.com/api/v1/webhooks/retouch"'

Webhook notifications

Webhooks are an optional addition to the standard polling flow.

When a job reaches a final state (completed or failed), a webhook worker will send a POST request to the URL specified in the hook parameter.

Webhooks will only be dispatched if the domain of the hook URL is explicitly allowed in the server webhook whitelist. Send the list of webhook domains to relu@retouch4.me before enabling this flow.

Request payload

The webhook payload is sent as application/json and follows this structure:

type WebhookRequest = {
  jobId: string;   // Unique identifier of the job (UUID)
  url: string;     // URL to download the processed image
  state: 'completed' | 'failed';
}

Webhook security and signature verification

To ensure that the webhook originated from our servers and has not been tampered with, every webhook request includes an X-Retouch-Signature header.

The signature header has the following format:

t=<timestamp>,v1=<signature>

To prevent replay attacks, reject any webhooks where the timestamp is too old.

Public key

WEBHOOK_PUBLIC_KEY=MCowBQYDK2VwAyEAaqw5RKSHecYfZQr3ErYWMBhQYMG2xkAflukuJXx/oM4=

Node.js verification example

Below is a complete example of how to parse the header and verify the Ed25519 signature using the native Node.js crypto module.

import { createPublicKey, verify } from 'crypto';

// Cache the public key to avoid recreating it on every request
let cachedWebhookPublicKey = null;

/**
 * Retrieves and formats the Ed25519 public key
 */
function getWebhookPublicKey() {
  if (cachedWebhookPublicKey) {
    return cachedWebhookPublicKey;
  }

  const publicKeyString = process.env.WEBHOOK_PUBLIC_KEY;
  if (!publicKeyString) {
    throw new Error('WEBHOOK_PUBLIC_KEY environment variable is not configured');
  }

  // Handle PEM format
  if (publicKeyString.includes('BEGIN')) {
    cachedWebhookPublicKey = createPublicKey(publicKeyString);
    return cachedWebhookPublicKey;
  }

  // Handle raw Base64 DER format
  cachedWebhookPublicKey = createPublicKey({
    key: Buffer.from(publicKeyString, 'base64'),
    format: 'der',
    type: 'spki',
  });
  return cachedWebhookPublicKey;
}

/**
 * Parses the X-Retouch-Signature header into its components
 */
function parseSignatureHeader(header) {
  const parts = header.split(',').reduce((acc, part) => {
    const [key, value] = part.split('=');
    acc[key] = value;
    return acc;
  }, {});
  return { timestamp: parts['t'], signatureB64: parts['v1'] };
}

/**
 * Webhook handler middleware
 */
export function verifyWebhook(req) {
  const signatureHeader = req.header('X-Retouch-Signature');
  if (!signatureHeader) {
    throw new Error('X-Retouch-Signature header is missing');
  }

  // 1. Verify timestamp to prevent replay attacks (10 seconds tolerance)
  const { timestamp, signatureB64 } = parseSignatureHeader(signatureHeader);
  const timestampSec = Number.parseInt(timestamp, 10);
  const nowSec = Math.floor(Date.now() / 1000);
  if (!Number.isFinite(timestampSec) || (nowSec - timestampSec > 10)) {
    throw new Error('Webhook rejected: Timestamp expired (older than 10 seconds)');
  }

  /* 2. Verify Ed25519 signature
   * Note: getRawBodyForVerification(req) should return the exact raw string or buffer
   * of the request body before any JSON parsing.
   */
  const rawBody = getRawBodyForVerification(req);

  const isValid = verify(
    null, // Algorithm is determined by the key type for Ed25519
    Buffer.from(rawBody, 'utf-8'),
    getWebhookPublicKey(),
    Buffer.from(signatureB64, 'base64')
  );

  if (!isValid) {
    throw new Error('Webhook rejected: Invalid signature');
  }

  return true; // Verification passed
}

Use the exact raw request body for signature verification before any JSON parsing or mutation.

Back to Cloud Retouching API documentation