> ## Documentation Index
> Fetch the complete documentation index at: https://edenai-docs-webhook-signature-public-key.mintlify.site/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-07-10T00: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. Verify each request against Eden AI's **public key** below. Save it as `edenai_webhook_rsa.pub.pem` next to your handler — this is the file the examples load.

```text edenai_webhook_rsa.pub.pem theme={null}
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA2fQneVfTrhkDdlvqs2Vi
T0tZ9PCkL0vHanZMc+6YyvvewIQRp1bH8Q13gYDf0t/tCKhVJqnPQ7qXekhtBrC1
uMAf/qDhUhkKB0w9Xt5DXbEZaxFQjrlDSHwcWdhH+BOAVDxQvaDuWveS8/kEIuo8
x/QLNOVTrUSKnjXnnqZ2k6OeD0WHv/6bLqPjFxdwL+4hi0nw1O57IfW7Oqzcd5JF
X1Aq8nU4N0P8PEetUl84LKCiiT801wKOksUJUtjGHqdtOqP4g0Vk0w5BtBDPVPyF
eQNLpALTxeG2WUtNtOiZCl0757K6dY13FYR86e7KUlQzaZhy2vBKtghi5Rf03puk
8fTS8zuO5LSX9GmHxZ1aywIvx6kcjp52S52/q6ZdXuTe+0DCxjoFCnJ3C/6cNphE
pZggShdZUtJFo85ImMRAesD0+hjeHuhqGuis/i1JOHzYcSpKVCaksujrscR+m7Id
1XrF/DY4gF8hizGHsEcurP+A6epUblTDtg/F0foYfitfAgMBAAE=
-----END PUBLIC KEY-----
```

The signature is built as follows:

1. The payload is serialized to **canonical JSON**: keys sorted alphabetically, 2-space indent, UTF-8 with non-ASCII characters left as-is (not `\u`-escaped). This is exactly `orjson.dumps(payload, option=OPT_INDENT_2 | OPT_SORT_KEYS)`.
2. `sha256(canonical_json_bytes)` is computed and its **hex digest** (a 64-character string) is taken.
3. That hex digest string is signed with RSA PKCS1 v1.5 over SHA-256.
4. The signature is hex-encoded and sent in the `X-Edenai-Signature` header.

The delivered request body is compact JSON, so you must re-serialize it into the canonical form above before hashing — do **not** hash the raw body, and 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.

  Number formatting differs across languages too: a whole-number float in the payload (for example a `1.0` confidence score) serializes as `1.0` on the Eden AI side but as `1` with JavaScript's `JSON.stringify`, which breaks verification. If your payloads can contain such values, verify from a Python service or normalize the numbers before hashing.
</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>
