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

# Webhooks

> Real-time notifications when your videos are ready

## Overview

Webhooks allow you to receive real-time notifications when your video processing completes, instead of polling the API repeatedly.

<Info>
  **Recommended**: Always use webhooks for production applications. Polling is inefficient and can hit rate limits.
</Info>

## How Webhooks Work

<Steps>
  <Step title="Provide Webhook URL">
    When creating a video, include your webhook URL

    ```javascript theme={null}
    {
      "type": "class",
      "script": "Your script...",
      "webhook": "https://yoursite.com/api/webhook"
    }
    ```
  </Step>

  <Step title="Video Processing">
    Hooked processes your video (typically 2-5 minutes)
  </Step>

  <Step title="Receive Notification">
    When complete, Hooked sends a POST request to your webhook URL with video details
  </Step>

  <Step title="Return 200 OK">
    Your endpoint must return a 200 status code to confirm receipt
  </Step>
</Steps>

## Webhook Payload

When your video is ready, you'll receive this payload:

```json theme={null}
{
  "event": "project.completed",
  "projectId": "proj_abc123",
  "status": "completed",
  "video": {
    "id": "video_xyz789",
    "url": "https://cdn.hooked.so/videos/xyz789.mp4",
    "duration": 45,
    "thumbnail": "https://cdn.hooked.so/thumbnails/xyz789.jpg",
    "aspectRatio": "9:16",
    "resolution": "1080x1920"
  },
  "createdAt": "2024-01-15T10:30:00Z",
  "completedAt": "2024-01-15T10:33:00Z"
}
```

### Payload Fields

<ResponseField name="event" type="string">
  Event type: `project.completed` or `project.failed`
</ResponseField>

<ResponseField name="projectId" type="string">
  The project/video ID
</ResponseField>

<ResponseField name="status" type="string">
  Status: `completed` or `failed`
</ResponseField>

<ResponseField name="video" type="object">
  Video details (only present if status is `completed`)

  <Expandable title="video object">
    <ResponseField name="url" type="string">
      Direct download URL for the video (expires in 7 days)
    </ResponseField>

    <ResponseField name="duration" type="number">
      Video duration in seconds
    </ResponseField>

    <ResponseField name="thumbnail" type="string">
      URL to video thumbnail image
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="message" type="string">
  Error message (only present if status is `failed`)
</ResponseField>

## Implementing a Webhook Endpoint

### Node.js (Express)

```javascript theme={null}
const express = require('express');
const app = express();

app.use(express.json());

app.post('/api/webhook', async (req, res) => {
  const { event, projectId, status, video } = req.body;
  
  console.log(`Received webhook: ${event} for ${projectId}`);
  
  if (event === 'project.completed' && status === 'completed') {
    // Video is ready!
    console.log('Video URL:', video.url);
    console.log('Duration:', video.duration, 'seconds');
    
    try {
      // Save to database
      await saveVideoToDatabase({
        projectId,
        videoUrl: video.url,
        thumbnail: video.thumbnail,
        duration: video.duration
      });
      
      // Notify user
      await notifyUser(projectId, video.url);
      
      // Download and store (optional)
      await downloadAndStore(video.url, projectId);
      
    } catch (error) {
      console.error('Error processing webhook:', error);
      // Still return 200 to prevent retries for application errors
    }
  } else if (event === 'project.failed') {
    console.error('Video failed:', req.body.message);
    await notifyUserOfFailure(projectId);
  }
  
  // IMPORTANT: Always return 200
  res.status(200).send('OK');
});

app.listen(3000);
```

### Python (FastAPI)

```python theme={null}
from fastapi import FastAPI, Request
import httpx

app = FastAPI()

@app.post("/api/webhook")
async def webhook_handler(request: Request):
    payload = await request.json()
    
    event = payload.get("event")
    project_id = payload.get("projectId")
    status = payload.get("status")
    
    print(f"Received webhook: {event} for {project_id}")
    
    if event == "project.completed" and status == "completed":
        video = payload.get("video")
        
        # Save to database
        await save_video_to_db({
            "project_id": project_id,
            "video_url": video["url"],
            "thumbnail": video["thumbnail"],
            "duration": video["duration"]
        })
        
        # Notify user
        await notify_user(project_id, video["url"])
        
    elif event == "project.failed":
        await notify_failure(project_id, payload.get("message"))
    
    # Always return 200
    return {"status": "ok"}
```

### Next.js API Route

```typescript theme={null}
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  try {
    const payload = await req.json();
    
    const { event, projectId, status, video } = payload;
    
    if (event === 'project.completed' && status === 'completed') {
      // Process completed video
      await processCompletedVideo({
        projectId,
        videoUrl: video.url,
        thumbnail: video.thumbnail,
        duration: video.duration
      });
    }
    
    return NextResponse.json({ received: true }, { status: 200 });
    
  } catch (error) {
    console.error('Webhook error:', error);
    // Still return 200 to prevent retries
    return NextResponse.json({ error: 'Internal error' }, { status: 200 });
  }
}
```

## Webhook Security

### Verify Webhook Source

<Warning>
  Always verify webhooks are coming from Hooked. Check the request origin and consider implementing HMAC signature verification.
</Warning>

```javascript theme={null}
// Verify request came from Hooked's IP ranges
const allowedIPs = ['52.1.2.3', '54.2.3.4']; // Example IPs

app.post('/api/webhook', (req, res) => {
  const clientIP = req.ip || req.headers['x-forwarded-for'];
  
  if (!allowedIPs.includes(clientIP)) {
    return res.status(403).send('Forbidden');
  }
  
  // Process webhook...
});
```

### Use HTTPS

<Check>
  **Always use HTTPS** for your webhook endpoints to ensure data is encrypted in transit.
</Check>

```
✅ https://yoursite.com/webhook
❌ http://yoursite.com/webhook
```

### Secret Tokens

Include a secret token in your webhook URL for additional security:

```javascript theme={null}
// When creating video
{
  "webhook": "https://yoursite.com/webhook?token=your_secret_token"
}

// In your webhook handler
app.post('/webhook', (req, res) => {
  const token = req.query.token;
  
  if (token !== process.env.WEBHOOK_SECRET) {
    return res.status(401).send('Unauthorized');
  }
  
  // Process webhook...
});
```

## Retry Logic

If your endpoint doesn't return a 200 status code, Hooked will retry:

* **1st retry**: After 1 minute
* **2nd retry**: After 5 minutes
* **3rd retry**: After 15 minutes

After 3 failed attempts, no more retries will be made.

### Best Practices for Retries

```javascript theme={null}
app.post('/webhook', async (req, res) => {
  // Return 200 FIRST
  res.status(200).send('OK');
  
  // Then process asynchronously
  setImmediate(async () => {
    try {
      await processWebhook(req.body);
    } catch (error) {
      // Handle error without affecting the HTTP response
      console.error('Async processing error:', error);
      await logToErrorTracking(error);
    }
  });
});
```

## Testing Webhooks

### Local Development with ngrok

```bash theme={null}
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js  # Running on localhost:3000

# Create tunnel
ngrok http 3000

# Use the ngrok URL as your webhook
# https://abc123.ngrok.io/api/webhook
```

### Webhook Testing Sites

<CardGroup cols={2}>
  <Card title="webhook.site" icon="browser">
    Get a temporary webhook URL for testing

    [https://webhook.site](https://webhook.site)
  </Card>

  <Card title="requestbin.com" icon="inbox">
    Inspect webhook payloads

    [https://requestbin.com](https://requestbin.com)
  </Card>
</CardGroup>

### Test Webhook Endpoint

```bash theme={null}
# Test your webhook manually
curl -X POST https://yoursite.com/api/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "event": "project.completed",
    "projectId": "test_123",
    "status": "completed",
    "video": {
      "url": "https://example.com/test.mp4",
      "duration": 30,
      "thumbnail": "https://example.com/thumb.jpg"
    }
  }'
```

## Common Issues

<AccordionGroup>
  <Accordion title="Webhook not received" icon="circle-xmark">
    **Possible causes:**

    * Webhook URL not publicly accessible
    * Firewall blocking Hooked's IP addresses
    * Endpoint returning non-200 status code

    **Solution:** Test with webhook.site first, then gradually move to your endpoint
  </Accordion>

  <Accordion title="Duplicate webhooks" icon="clone">
    **Possible causes:**

    * Your endpoint took too long to respond
    * Endpoint returned non-200, triggering retry

    **Solution:** Implement idempotency using projectId

    ```javascript theme={null}
    const processedProjects = new Set();

    app.post('/webhook', (req, res) => {
      const { projectId } = req.body;
      
      if (processedProjects.has(projectId)) {
        return res.status(200).send('Already processed');
      }
      
      processedProjects.add(projectId);
      // Process webhook...
    });
    ```
  </Accordion>

  <Accordion title="Slow webhook processing" icon="hourglass">
    **Problem:** Your endpoint is slow, causing timeouts

    **Solution:** Return 200 immediately, process asynchronously

    ```javascript theme={null}
    app.post('/webhook', async (req, res) => {
      // Respond immediately
      res.status(200).send('OK');
      
      // Process in background
      processWebhookAsync(req.body);
    });
    ```
  </Accordion>
</AccordionGroup>

## Webhook Events

Currently supported events:

| Event               | Description                                        |
| ------------------- | -------------------------------------------------- |
| `project.completed` | Video successfully generated and ready to download |
| `project.failed`    | Video generation failed due to an error            |

## Next Steps

<CardGroup cols={2}>
  <Card title="Error Handling" icon="triangle-exclamation" href="/guides/error-handling">
    Learn how to handle errors gracefully
  </Card>

  <Card title="Complete Workflow" icon="diagram-project" href="/guides/complete-workflow">
    See webhooks in action
  </Card>
</CardGroup>
