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

# Webhooks

> Webhooks work in tandem with the queue system explained above, it is another way to interact with our queue. By providing us a webhook endpoint you get notified when the request is done as opposed to polling it.

Webhooks let you receive a notification at a URL you control when an asynchronous request completes, instead of polling the queue for results. This is especially useful for long-running tasks like model training or video generation where you don't want to keep a connection open. You can manage and monitor your webhook endpoints from the [Webhooks dashboard](https://fal.ai/dashboard/webhooks).

Setting up a webhook is straightforward. Pass a `webhook_url` when submitting a request to the [queue](/documentation/model-apis/inference/queue), and fal will POST the result to that URL when processing completes.

<CodeGroup>
  ```python Python theme={null}
  import fal_client

  handler = fal_client.submit("fal-ai/flux/dev",
      arguments={"prompt": "Photo of a cute dog"},
      webhook_url="https://url.to.your.app/api/fal/webhook",
  )

  print(f"Request submitted: {handler.request_id}")
  ```

  ```python Python (async) theme={null}
  import fal_client

  handler = await fal_client.submit_async("fal-ai/flux/dev",
      arguments={"prompt": "Photo of a cute dog"},
      webhook_url="https://url.to.your.app/api/fal/webhook",
  )

  print(f"Request submitted: {handler.request_id}")
  ```

  ```javascript JavaScript theme={null}
  import { fal } from "@fal-ai/client";

  const { request_id } = await fal.queue.submit("fal-ai/flux/dev", {
    input: { prompt: "Photo of a cute dog" },
    webhookUrl: "https://url.to.your.app/api/fal/webhook",
  });

  console.log(`Request submitted: ${request_id}`);
  ```

  ```bash cURL theme={null}
  curl --request POST \
    --url 'https://queue.fal.run/fal-ai/flux/dev?fal_webhook=https://url.to.your.app/api/fal/webhook' \
    --header "Authorization: Key $FAL_KEY" \
    --header 'Content-Type: application/json' \
    --data '{
    "prompt": "Photo of a cute dog"
  }'
  ```
</CodeGroup>

The request will be queued and you will get a response with the `request_id` and `gateway_request_id`:

```json theme={null}
{
  "request_id": "024ca5b1-45d3-4afd-883e-ad3abe2a1c4d",
  "gateway_request_id": "024ca5b1-45d3-4afd-883e-ad3abe2a1c4d"
}
```

These two will be mostly the same, but if the request failed and was retried, `gateway_request_id` will have the value of the last tried request, while
`request_id` will be the value used in the queue API.

Once the request is done processing in the queue, a `POST` request is made to the webhook URL, passing the request info and the resulting `payload`. The `status` indicates whether the request was successful or not.

<Note>
  **When to use it?**

  Webhooks are particularly useful for requests that can take a while to process and/or the result is not needed immediately. For example, if you are training a model, which is a process than can take several minutes or even hours, webhooks could be the perfect tool for the job.
</Note>

### Successful result

The following is an example of a successful request:

```json highlight={4} theme={null}
{
  "request_id": "123e4567-e89b-12d3-a456-426614174000",
  "gateway_request_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "OK",
  "payload": {
    "images": [
      {
        "url": "https://url.to/image.png",
        "content_type": "image/png",
        "file_name": "image.png",
        "file_size": 1824075,
        "width": 1024,
        "height": 1024
      }
    ],
    "seed": 196619188014358660
  }
}
```

### Response errors

When an error happens, the `status` will be `ERROR`. The `error` property will contain a message and the `payload` will provide the error details. For example, if you forget to pass the required `model_name` parameter, you will get the following response:

```json highlight={4,5} theme={null}
{
  "request_id": "123e4567-e89b-12d3-a456-426614174000",
  "gateway_request_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "ERROR",
  "error": "Invalid status code: 422",
  "payload": {
    "detail": [
      {
        "loc": ["body", "prompt"],
        "msg": "field required",
        "type": "value_error.missing"
      }
    ]
  }
}
```

### Payload errors

For the webhook to include the payload, it must be valid JSON. So if there is an error serializing it, `payload` is set to `null` and a `payload_error` will include details about the error.

```json highlight={5,6} theme={null}
{
  "request_id": "123e4567-e89b-12d3-a456-426614174000",
  "gateway_request_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "OK",
  "payload": null,
  "payload_error": "Response payload is not JSON serializable. Either return a JSON serializable object or use the queue endpoint to retrieve the response."
}
```

### Retry policy

Initial webhook deliveries have a 15-second timeout. If a delivery exceeds this time or fails to deliver the payload, it will retry 10 times in the span of 2 hours. Design your webhook handler to be idempotent and tolerate repeat deliveries for the same `request_id`.

### Webhook IP Ranges

If your infrastructure requires allowlisting IP addresses for incoming webhook requests, you can retrieve the current list of IP ranges used by fal.ai webhooks from the metadata endpoint:

```bash theme={null}
curl https://api.fal.ai/v1/meta
```

The response includes the `webhook_ip_ranges` field containing all IP addresses that may send webhook requests:

```json theme={null}
{
  "webhook_ip_ranges": [
    "34.123.59.101/32",
    "34.135.41.243/32",
    "35.239.83.87/32",
    "104.198.204.37/32",
    "34.56.20.205/32",
    "34.170.94.127/32",
    "35.224.184.236/32",
    "136.114.56.197/32",
    "34.29.37.237/32",
    "35.225.160.28/32",
    "34.56.205.145/32",
    "34.59.170.72/32",
    "34.10.147.45/32",
    "104.198.64.245/32",
    "34.9.1.255/32"
  ]
}
```

<Note>
  These IP ranges may change over time. We recommend fetching this list periodically to keep your allowlist up to date.
</Note>

### Verifying Your Webhook

To ensure the security and integrity of incoming webhook requests, you must verify that they originate from the expected source. This involves validating a cryptographic signature included in the request using a set of public keys. Below is a step-by-step guide to the verification process, followed by example implementations in Python and JavaScript.

#### Verification Process

<Steps>
  <Step title="Fetch the JSON Web Key Set (JWKS)">
    * Retrieve the public keys from the JWKS endpoint: `https://rest.fal.ai/.well-known/jwks.json`.
    * The JWKS contains a list of public keys in JSON format, each with an `x` field holding a base64url-encoded ED25519 public key.
    * **Note**: The JWKS is cacheable to reduce network requests. Ensure your implementation caches the keys and refreshes them after the cache duration expires. Do not cache longer than 24 hours since they can change.
  </Step>

  <Step title="Extract Required Headers">
    * Obtain the following headers from the incoming webhook request:
      * `X-Fal-Webhook-Request-Id`: The unique request ID.
      * `X-Fal-Webhook-User-Id`: Your user ID.
      * `X-Fal-Webhook-Timestamp`: The timestamp when the request was generated (in Unix epoch seconds).
      * `X-Fal-Webhook-Signature`: The cryptographic signature in hexadecimal format.
    * If any header is missing, the request is invalid.
  </Step>

  <Step title="Verify the Timestamp">
    * Compare the `X-Fal-Webhook-Timestamp` with the current Unix timestamp.
    * Allow a leeway of ±5 minutes (300 seconds) to account for clock skew and network delays.
    * If the timestamp differs by more than 300 seconds, reject the request to prevent replay attacks.
  </Step>

  <Step title="Construct the Message">
    * Compute the SHA-256 hash of the request body (raw bytes, not JSON-parsed).
    * Concatenate the following in strict order, separated by newline characters (`\n`):
      * `X-Fal-Webhook-Request-Id`
      * `X-Fal-Webhook-User-Id`
      * `X-Fal-Webhook-Timestamp`
      * Hex-encoded SHA-256 hash of the request body
    * Encode the resulting string as UTF-8 bytes to form the message to verify.
  </Step>

  <Step title="Verify the Signature">
    * Decode the `X-Fal-Webhook-Signature` from hexadecimal to bytes.
    * For each public key in the JWKS:
      * Decode the `x` field from base64url to bytes.
      * Use an ED25519 verification function (e.g., from PyNaCl in Python or libsodium in JavaScript) to verify the signature against the constructed message.
    * If any key successfully verifies the signature, the request is valid.
    * If no key verifies the signature, the request is invalid.
  </Step>
</Steps>

#### Example Implementations

Below are simplified functions to verify webhook signatures by passing the header values and request body directly. These examples handle the verification process as described above and include JWKS caching.

<Tabs>
  <Tab title="python" icon="python">
    **Install dependencies**:

    ```bash theme={null}
    pip install pynacl requests
    ```

    **Verification function**:

    ```python theme={null}
    import base64
    import hashlib
    import time
    from typing import Optional
    import requests
    from nacl.signing import VerifyKey
    from nacl.exceptions import BadSignatureError
    from nacl.encoding import HexEncoder

    JWKS_URL = "https://rest.fal.ai/.well-known/jwks.json"
    JWKS_CACHE_DURATION = 24 * 60 * 60  # 24 hours in seconds
    _jwks_cache = None
    _jwks_cache_time = 0

    def fetch_jwks() -> list:
        """Fetch and cache JWKS, refreshing after 24 hours."""
        global _jwks_cache, _jwks_cache_time
        current_time = time.time()
        if _jwks_cache is None or (current_time - _jwks_cache_time) > JWKS_CACHE_DURATION:
            response = requests.get(JWKS_URL, timeout=10)
            response.raise_for_status()
            _jwks_cache = response.json().get("keys", [])
            _jwks_cache_time = current_time
        return _jwks_cache

    def verify_webhook_signature(
        request_id: str,
        user_id: str,
        timestamp: str,
        signature_hex: str,
        body: bytes
    ) -> bool:
        """
        Verify a webhook signature using provided headers and body.

        Args:
            request_id: Value of X-Fal-Webhook-Request-Id header.
            user_id: Value of X-Fal-Webhook-User-Id header.
            timestamp: Value of X-Fal-Webhook-Timestamp header.
            signature_hex: Value of X-Fal-Webhook-Signature header (hex-encoded).
            body: Raw request body as bytes.

        Returns:
            bool: True if the signature is valid, False otherwise.
        """
        # Validate timestamp (within ±5 minutes)
        try:
            timestamp_int = int(timestamp)
            current_time = int(time.time())
            if abs(current_time - timestamp_int) > 300:
                print("Timestamp is too old or in the future.")
                return False
        except ValueError:
            print("Invalid timestamp format.")
            return False

        # Construct the message to verify
        try:
            message_parts = [
                request_id,
                user_id,
                timestamp,
                hashlib.sha256(body).hexdigest()
            ]
            if any(part is None for part in message_parts):
                print("Missing required header value.")
                return False
            message_to_verify = "\n".join(message_parts).encode("utf-8")
        except Exception as e:
            print(f"Error constructing message: {e}")
            return False

        # Decode signature
        try:
            signature_bytes = bytes.fromhex(signature_hex)
        except ValueError:
            print("Invalid signature format (not hexadecimal).")
            return False

        # Fetch public keys
        try:
            public_keys_info = fetch_jwks()
            if not public_keys_info:
                print("No public keys found in JWKS.")
                return False
        except Exception as e:
            print(f"Error fetching JWKS: {e}")
            return False

        # Verify signature with each public key
        for key_info in public_keys_info:
            try:
                public_key_b64url = key_info.get("x")
                if not isinstance(public_key_b64url, str):
                    continue
                public_key_bytes = base64.urlsafe_b64decode(public_key_b64url)
                verify_key = VerifyKey(public_key_bytes.hex(), encoder=HexEncoder)
                verify_key.verify(message_to_verify, signature_bytes)
                return True
            except (BadSignatureError, Exception) as e:
                print(f"Verification failed with a key: {e}")
                continue

        print("Signature verification failed with all keys.")
        return False
    ```
  </Tab>

  <Tab title="javascript" icon="js">
    **Install dependencies**:

    ```bash theme={null}
    npm install libsodium-wrappers node-fetch
    ```

    **Verification function**:

    ```javascript theme={null}
    const crypto = require('crypto');
    const sodium = require('libsodium-wrappers');
    const fetch = require('node-fetch');

    const JWKS_URL = 'https://rest.fal.ai/.well-known/jwks.json';
    const JWKS_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
    let jwksCache = null;
    let jwksCacheTime = 0;

    async function fetchJwks() {
        const currentTime = Date.now();
        if (!jwksCache || (currentTime - jwksCacheTime) > JWKS_CACHE_DURATION) {
            const response = await fetch(JWKS_URL, { timeout: 10000 });
            if (!response.ok) throw new Error(`JWKS fetch failed: ${response.status}`);
            jwksCache = (await response.json()).keys || [];
            jwksCacheTime = currentTime;
        }
        return jwksCache;
    }

    async function verifyWebhookSignature(requestId, userId, timestamp, signatureHex, body) {
        /*
         * Verify a webhook signature using provided headers and body.
         *
         * @param {string} requestId - Value of x-fal-webhook-request-id header.
         * @param {string} userId - Value of x-fal-webhook-user-id header.
         * @param {string} timestamp - Value of x-fal-webhook-timestamp header.
         * @param {string} signatureHex - Value of x-fal-webhook-signature header (hex-encoded).
         * @param {Buffer} body - Raw request body as a Buffer.
         * @returns {Promise<boolean>} True if the signature is valid, false otherwise.
         */
        await sodium.ready;

        // Validate timestamp (within ±5 minutes)
        try {
            const timestampInt = parseInt(timestamp, 10);
            const currentTime = Math.floor(Date.now() / 1000);
            if (Math.abs(currentTime - timestampInt) > 300) {
                console.error('Timestamp is too old or in the future.');
                return false;
            }
        } catch (e) {
            console.error('Invalid timestamp format:', e);
            return false;
        }

        // Construct the message to verify
        try {
            const messageParts = [
                requestId,
                userId,
                timestamp,
                crypto.createHash('sha256').update(body).digest('hex')
            ];
            if (messageParts.some(part => part == null)) {
                console.error('Missing required header value.');
                return false;
            }
            const messageToVerify = messageParts.join('\n');
            const messageBytes = Buffer.from(messageToVerify, 'utf-8');

            // Decode signature
            let signatureBytes;
            try {
                signatureBytes = Buffer.from(signatureHex, 'hex');
            } catch (e) {
                console.error('Invalid signature format (not hexadecimal).');
                return false;
            }

            // Fetch public keys
            let publicKeysInfo;
            try {
                publicKeysInfo = await fetchJwks();
                if (!publicKeysInfo.length) {
                    console.error('No public keys found in JWKS.');
                    return false;
                }
            } catch (e) {
                console.error('Error fetching JWKS:', e);
                return false;
            }

            // Verify signature with each public key
            for (const keyInfo of publicKeysInfo) {
                try {
                    const publicKeyB64Url = keyInfo.x;
                    if (typeof publicKeyB64Url !== 'string') continue;
                    const publicKeyBytes = Buffer.from(publicKeyB64Url, 'base64url');
                    const isValid = sodium.crypto_sign_verify_detached(signatureBytes, messageBytes, publicKeyBytes);
                    if (isValid) return true;
                } catch (e) {
                    console.error('Verification failed with a key:', e);
                    continue;
                }
            }

            console.error('Signature verification failed with all keys.');
            return false;
        } catch (e) {
            console.error('Error constructing message:', e);
            return false;
        }
    }
    ```
  </Tab>
</Tabs>

#### Usage Notes

* **Caching the JWKS**: The JWKS can be cached for 24 hours to minimize network requests. The example implementations include basic in-memory caching.
* **Timestamp Validation**: The ±5-minute leeway ensures robustness against minor clock differences. Adjust this value if your use case requires stricter or looser validation.
* **Error Handling**: The examples include comprehensive error handling for missing headers, invalid signatures, and network issues. Log errors appropriately for debugging.
* **Framework Integration**: For frameworks like FastAPI (Python) or Express (JavaScript), ensure the raw request body is accessible. For Express, use `express.raw({ type: 'application/json' })` middleware before JSON parsing.
