> ## Documentation Index
> Fetch the complete documentation index at: https://www.edenai.co/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Webhooks let you receive results from async Universal AI jobs via HTTP callbacks instead of polling.

export const TechArticleSchema = ({title, description, path, articleSection, about, proficiencyLevel = "Beginner", dependencies, keywords = [], datePublished, dateModified, image, inLanguage = "en"}) => {
  const baseUrl = "https://www.edenai.co/docs";
  const canonicalUrl = `${baseUrl}/${path}`.replace(/\/+$/, "");
  const ogParams = new URLSearchParams({
    division: articleSection || "",
    title: title || "",
    description: description || ""
  });
  const resolvedImage = image || `https://edenai.mintlify.app/_mintlify/api/og?${ogParams.toString()}`;
  const data = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    "@id": `${canonicalUrl}#techarticle`,
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": canonicalUrl
    },
    headline: title,
    name: title,
    description: description,
    url: canonicalUrl,
    inLanguage: inLanguage,
    isPartOf: {
      "@type": "WebSite",
      name: "Eden AI Documentation",
      url: baseUrl
    },
    author: [{
      "@type": "Organization",
      name: "Eden AI",
      url: "https://www.edenai.co/"
    }],
    publisher: {
      "@type": "Organization",
      name: "Eden AI",
      url: "https://www.edenai.co/",
      logo: {
        "@type": "ImageObject",
        url: "https://www.edenai.co/assets/logo.png"
      }
    }
  };
  if (articleSection) data.articleSection = articleSection;
  if (about) data.about = {
    "@type": "Thing",
    name: about
  };
  if (proficiencyLevel) data.proficiencyLevel = proficiencyLevel;
  if (dependencies) data.dependencies = dependencies;
  if (keywords && keywords.length) data.keywords = keywords;
  if (datePublished) data.datePublished = datePublished;
  if (dateModified) data.dateModified = dateModified;
  data.image = Array.isArray(resolvedImage) ? resolvedImage : [resolvedImage];
  const json = JSON.stringify(data);
  const schemaId = `techarticle-${canonicalUrl}`;
  React.useEffect(() => {
    if (typeof document === "undefined") return;
    document.querySelectorAll(`script[data-schema-id="${schemaId}"]`).forEach(n => n.remove());
    const script = document.createElement("script");
    script.type = "application/ld+json";
    script.dataset.schemaId = schemaId;
    script.textContent = json;
    document.head.appendChild(script);
    return () => script.remove();
  }, [json, schemaId]);
  return null;
};

<TechArticleSchema title={"Webhooks"} description={"Webhooks let you receive results from async Universal AI jobs via HTTP callbacks instead of polling."} path="v3/expert-models/webhooks" articleSection="Expert Models" about={"AI API"} proficiencyLevel="Intermediate" keywords={["Eden AI", "AI API", "expert models", "multi-provider"]} datePublished="2026-05-06T00:00:00Z" dateModified="2026-05-07T00:00:00Z" />

Webhooks let you receive results from async Universal AI jobs via HTTP callbacks instead of polling. When a job completes, Eden AI sends a signed `POST` request to the URL you provided.

## How It Works

1. Submit an async request with a `webhook_receiver` in the payload.
2. Eden AI processes the job in the background.
3. On completion, Eden AI sends a signed `POST` request to your `webhook_receiver` with the result.

## Request Parameters

| Field                     | Type           | Required | Description                                                                                                                             |
| ------------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook_receiver`        | `string` (URL) | no       | HTTPS URL that will receive the `POST` when the job completes or fails. Validated against SSRF.                                         |
| `user_webhook_parameters` | `object`       | no       | Free-form JSON object echoed back as `user_parameters` in the webhook payload. Useful for correlating the callback to your own records. |

## Webhook Headers

Every webhook request Eden AI sends includes these headers:

| Header                    | Example value        | Description                                     |
| ------------------------- | -------------------- | ----------------------------------------------- |
| `Content-Type`            | `application/json`   | Body is always JSON.                            |
| `User-Agent`              | `EdenAI/Ai-Features` | Identifies the sender.                          |
| `X-Edenai-Webhook`        | `true`               | Flag marking the request as an Eden AI webhook. |
| `X-Edenai-Signature`      | `a8f3...` (hex)      | RSA PKCS1 v1.5 signature of the payload.        |
| `X-Edenai-Hash-Algorithm` | `SHA256`             | Hash algorithm used to produce the signature.   |

## Webhook Payload

```json theme={null}
{
  "event": "async_job_completed",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "success",
  "feature": "ocr",
  "subfeature": "ocr_async",
  "provider": "amazon",
  "model": null,
  "created_at": "2026-04-21T12:00:00+00:00",
  "finished_at": "2026-04-21T12:01:30+00:00",
  "output": { "raw_text": "..." },
  "user_parameters": { "internal_ref": "order-42" }
}
```

| Field               | Type                  | Description                                                                              |
| ------------------- | --------------------- | ---------------------------------------------------------------------------------------- |
| `event`             | `string`              | Always `"async_job_completed"`.                                                          |
| `job_id`            | `string` (UUID)       | ID of the completed job.                                                                 |
| `status`            | `"success" \| "fail"` | Final job status.                                                                        |
| `feature`           | `string`              | Feature name (e.g. `"ocr"`, `"audio"`).                                                  |
| `subfeature`        | `string`              | Subfeature name (e.g. `"ocr_async"`).                                                    |
| `provider`          | `string`              | Provider that produced the result.                                                       |
| `model`             | `string \| null`      | Provider-specific model, if any.                                                         |
| `created_at`        | `string` (ISO 8601)   | When the job was created.                                                                |
| `finished_at`       | `string` (ISO 8601)   | When the job finished.                                                                   |
| `output`            | `object`              | Normalized result. Present only on `status: "success"`.                                  |
| `error`             | `object`              | `{ "message": "..." }`. Present only on `status: "fail"`.                                |
| `original_response` | `any`                 | Raw provider response. Only present when the request had `show_original_response: true`. |
| `user_parameters`   | `object`              | Echo of `user_webhook_parameters`. Absent if not set.                                    |

## Signature Verification

Every webhook is signed with Eden AI's RSA private key so you can verify it was not forged or tampered with in transit.

<Tip>
  Ask Eden AI support for the webhook **public key** (`webhook_rsa.pub.pem`) and store it with your service configuration.
</Tip>

The signature is built as follows:

1. The payload is serialized with canonical JSON (sorted keys, 2-space indent, UTF-8).
2. `sha256(canonical_json_bytes)` is computed and its **hex digest** is taken.
3. That hex digest is signed with RSA PKCS1 v1.5 / SHA-256.
4. The hex-encoded signature is sent in `X-Edenai-Signature`.

To verify, reproduce the same canonical JSON before hashing — do **not** re-serialize with default formatting.

## Example

<Tabs>
  <Tab title="Python">
    Dependencies: `pip install requests flask pycryptodome orjson`

    **Sending the request:**

    ```python theme={null}
    import requests

    url = "https://api.edenai.run/v3/universal-ai/async"
    headers = {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
    }
    payload = {
        "model": "ocr/ocr_async/amazon",
        "input": {"file": "YOUR_FILE_UUID_OR_URL"},
        "webhook_receiver": "https://your-server.com/webhooks/edenai",
        "user_webhook_parameters": {"internal_ref": "order-42"},
    }
    response = requests.post(url, headers=headers, json=payload)
    print(response.json())  # Contains the job ID
    ```

    **Receiving and verifying the webhook (Flask):**

    ```python theme={null}
    import hashlib
    import orjson
    from flask import Flask, request, abort, jsonify
    from Crypto.Hash import SHA256
    from Crypto.PublicKey import RSA
    from Crypto.Signature import PKCS1_v1_5

    app = Flask(__name__)

    with open("edenai_webhook_rsa.pub.pem", "rb") as f:
        EDENAI_PUBLIC_KEY = RSA.import_key(f.read())


    def verify_signature(raw_body: bytes, signature_hex: str) -> bool:
        payload = orjson.loads(raw_body)
        canonical = orjson.dumps(
            payload,
            option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS,
        )
        hash_hex = hashlib.sha256(canonical).hexdigest()
        verifier = PKCS1_v1_5.new(EDENAI_PUBLIC_KEY)
        digest = SHA256.new(data=hash_hex.encode("utf-8"))
        try:
            return verifier.verify(digest, bytes.fromhex(signature_hex))
        except ValueError:
            return False


    @app.route("/webhooks/edenai", methods=["POST"])
    def handle_webhook():
        signature = request.headers.get("X-Edenai-Signature", "")
        if not verify_signature(request.get_data(), signature):
            abort(401, description="Invalid Eden AI webhook signature")

        payload = request.get_json()
        if payload["status"] == "success":
            print(f"Job {payload['job_id']} completed: {payload['output']}")
        else:
            print(f"Job {payload['job_id']} failed: {payload['error']['message']}")
        return jsonify({"received": True}), 200
    ```
  </Tab>

  <Tab title="JavaScript">
    Dependencies: `npm install express safe-stable-stringify`

    **Sending the request:**

    ```javascript theme={null}
    (async () => {
      const url = "https://api.edenai.run/v3/universal-ai/async";
      const headers = {
        Authorization: "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
      };
      const payload = {
        model: "ocr/ocr_async/amazon",
        input: { file: "YOUR_FILE_UUID_OR_URL" },
        webhook_receiver: "https://your-server.com/webhooks/edenai",
        user_webhook_parameters: { internal_ref: "order-42" },
      };
      const response = await fetch(url, {
        method: "POST",
        headers,
        body: JSON.stringify(payload),
      });
      console.log(await response.json()); // Contains the job ID
    })();
    ```

    **Receiving and verifying the webhook (Express):**

    ```javascript theme={null}
    const express = require("express");
    const crypto = require("crypto");
    const stringify = require("safe-stable-stringify");
    const fs = require("fs");

    const app = express();
    const EDENAI_PUBLIC_KEY = fs.readFileSync("edenai_webhook_rsa.pub.pem", "utf8");

    function verifySignature(rawBody, signatureHex) {
      const payload = JSON.parse(rawBody.toString("utf8"));
      const canonical = stringify(payload, null, 2); // sorted keys + 2-space indent
      const hashHex = crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
      const verifier = crypto.createVerify("RSA-SHA256");
      verifier.update(hashHex, "utf8");
      verifier.end();
      try {
        return verifier.verify(EDENAI_PUBLIC_KEY, Buffer.from(signatureHex, "hex"));
      } catch {
        return false;
      }
    }

    app.use("/webhooks/edenai", express.raw({ type: "application/json" }));

    app.post("/webhooks/edenai", (req, res) => {
      const signature = req.header("X-Edenai-Signature") || "";
      if (!verifySignature(req.body, signature)) {
        return res.status(401).send("Invalid Eden AI webhook signature");
      }
      const payload = JSON.parse(req.body.toString("utf8"));
      if (payload.status === "success") {
        console.log(`Job ${payload.job_id} completed:`, payload.output);
      } else {
        console.log(`Job ${payload.job_id} failed: ${payload.error.message}`);
      }
      res.status(200).json({ received: true });
    });

    app.listen(3000);
    ```
  </Tab>
</Tabs>

<Warning>
  Do **not** re-serialize the parsed JSON with defaults (e.g. `JSON.stringify(obj)` or `json.dumps(obj)` without `sort_keys=True, indent=2`) — any difference in spacing, key ordering, or unicode escaping will make the signature fail to verify.
</Warning>

## Retry Behavior

Eden AI retries webhook delivery up to **3 times** with exponential backoff between attempts (max delay 30 s) when the receiver returns a transient failure. Each attempt has a per-request timeout of 30 s.

| Outcome                    | Retried? |
| -------------------------- | -------- |
| HTTP 2xx                   | --       |
| HTTP 4xx                   | No       |
| HTTP 5xx                   | Yes      |
| Per-request timeout (30 s) | Yes      |
| Connection / network error | Yes      |

<Tip>
  Keep your webhook handler idempotent — Eden AI may deliver the same `job_id` more than once if a retry races with a slow response from your server.
</Tip>

## Webhook vs Polling

|                  | Webhooks                                | Polling                                                   |
| ---------------- | --------------------------------------- | --------------------------------------------------------- |
| **How it works** | Eden AI pushes the result to your URL   | You repeatedly call `GET /v3/universal-ai/async/{job_id}` |
| **Latency**      | Immediate notification on completion    | Depends on polling interval                               |
| **Efficiency**   | No wasted requests                      | Requires repeated API calls                               |
| **Setup**        | Requires a publicly accessible endpoint | Works from any client                                     |

## Next Steps

<CardGroup cols={2}>
  <Card title="Monitoring" icon="gear" href="/v3/general/monitoring">
    Track async job results and API usage in the dashboard
  </Card>
</CardGroup>
