Webhooks #
Receive real-time notifications when your book generation jobs complete.
Overview #
Instead of polling the status endpoint, you can provide a webhook_url when creating a book. Nellie will send a POST request to your URL when the job completes (successfully or with an error).
Benefits:
- No polling required
- Instant notification on completion
- Reduced API calls
- Event-driven architecture support
Quick Start #
1. Set Up Your Endpoint #
Create a publicly accessible HTTPS endpoint to receive webhooks:
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/nellie', methods=['POST'])
def handle_webhook():
data = request.json
if data['status'] == 'completed':
print(f"Book ready: {data['resultUrl']}")
# Download and process the book
else:
print(f"Book failed: {data.get('error')}")
return jsonify({'received': True}), 200
2. Include webhook_url in Your Request #
from nellie_api import Nellie
client = Nellie(api_key="nel_...")
book = client.books.create(
prompt="A mystery novel",
webhook_url="https://your-site.com/webhooks/nellie"
)
# No need to poll - you'll receive a webhook when done
print(f"Job started: {book.request_id}")
3. Handle the Webhook #
When the job completes, Nellie POSTs to your URL with the result.
Webhook Payload #
Success Payload #
{
"requestId": "abcd1234-ef56-7890-abcd-12ef34567890",
"status": "completed",
"completedAt": "2025-12-03T10:55:00Z",
"creditsUsed": 250,
"resultUrl": "https://api.nelliewriter.com/v1/download/abcd1234-ef56-7890-abcd-12ef34567890"
}
Failure Payload #
{
"requestId": "abcd1234-ef56-7890-abcd-12ef34567890",
"status": "failed",
"completedAt": "2025-12-03T10:35:00Z",
"creditsUsed": 0,
"resultUrl": null,
"error": "Insufficient credits"
}
Payload Fields #
| Field | Type | Description | |
|---|---|---|---|
requestId |
string | The job identifier | |
status |
string | "completed" or "failed" |
|
completedAt |
string | ISO 8601 completion timestamp | |
creditsUsed |
integer | Credits consumed (0 if failed) | |
resultUrl |
string | null | Download URL (null if failed) |
error |
string | Error description (only if failed) |
Signature Verification #
Nellie signs all webhook payloads so you can verify they’re authentic. The signature is in the X-Nellie-Signature header.
Getting Your Webhook Secret #
- Open the Nellie app
- Go to Settings → API Management
- Select the Webhooks tab
- Copy your Webhook Signing Secret
Your secret looks like: whsec_abc123def456...
Signature Format #
The X-Nellie-Signature header contains:
t=1701234567,v1=abc123def456...
t: Unix timestamp when the signature was createdv1: HMAC-SHA256 signature
Verification Algorithm #
- Parse the timestamp (
t) and signature (v1) from the header - Reject if timestamp is too old (>5 minutes)
- Concatenate:
{timestamp}.{raw_payload} - Compute HMAC-SHA256 using your webhook secret
- Compare signatures using constant-time comparison
Python Verification (Manual) #
import hmac
import hashlib
import time
def verify_webhook(request, secret):
# 1. Get the signature header
sig_header = request.headers.get('X-Nellie-Signature')
if not sig_header:
return False
# 2. Parse timestamp and signature
parts = dict(x.split('=', 1) for x in sig_header.split(','))
timestamp = parts.get('t')
signature = parts.get('v1')
if not timestamp or not signature:
return False
# 3. Reject old signatures (replay protection)
if abs(time.time() - int(timestamp)) > 300: # 5 minutes
return False
# 4. Get the raw payload
payload = request.get_data(as_text=True)
# 5. Compute expected signature
signed_content = f"{timestamp}.{payload}"
expected = hmac.new(
key=secret.encode(),
msg=signed_content.encode(),
digestmod=hashlib.sha256
).hexdigest()
# 6. Compare securely
return hmac.compare_digest(expected, signature)
Python Verification (SDK) #
The SDK provides a convenient helper:
from nellie_api import Webhook, WebhookSignatureError
import os
WEBHOOK_SECRET = os.environ.get('NELLIE_WEBHOOK_SECRET')
@app.route('/webhooks/nellie', methods=['POST'])
def handle_webhook():
payload = request.data
sig_header = request.headers.get('X-Nellie-Signature')
try:
event = Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
except WebhookSignatureError:
return 'Invalid signature', 400
# event is a Book object
if event.status == 'completed':
print(f"Book ready: {event.result_url}")
else:
print(f"Book failed: {event.error}")
return 'OK', 200
Node.js Verification #
const crypto = require('crypto');
function verifyWebhook(payload, sigHeader, secret) {
if (!sigHeader) return false;
// Parse header
const parts = Object.fromEntries(
sigHeader.split(',').map(x => x.split('='))
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
// Check timestamp (5 minute tolerance)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) return false;
// Compute expected signature
const signedContent = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express example
app.post('/webhooks/nellie', express.raw({type: 'application/json'}), (req, res) => {
const payload = req.body.toString();
const sigHeader = req.headers['x-nellie-signature'];
if (!verifyWebhook(payload, sigHeader, process.env.WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log(`Received webhook for ${event.requestId}: ${event.status}`);
res.status(200).send('OK');
});
Framework Examples #
Flask #
from flask import Flask, request, jsonify
from nellie_api import Webhook, WebhookSignatureError
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['NELLIE_WEBHOOK_SECRET']
@app.route('/webhooks/nellie', methods=['POST'])
def nellie_webhook():
try:
event = Webhook.construct_event(
request.data,
request.headers.get('X-Nellie-Signature'),
WEBHOOK_SECRET
)
except WebhookSignatureError:
return jsonify({'error': 'Invalid signature'}), 400
except ValueError:
return jsonify({'error': 'Invalid payload'}), 400
# Process the event
if event.is_successful():
# Download the result
download_book(event.result_url, event.request_id)
else:
# Handle failure
log_failure(event.request_id, event.error)
return jsonify({'received': True}), 200
FastAPI #
from fastapi import FastAPI, Request, HTTPException
from nellie_api import Webhook, WebhookSignatureError
import os
app = FastAPI()
WEBHOOK_SECRET = os.environ['NELLIE_WEBHOOK_SECRET']
@app.post('/webhooks/nellie')
async def nellie_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get('x-nellie-signature')
try:
event = Webhook.construct_event(payload, sig_header, WEBHOOK_SECRET)
except WebhookSignatureError:
raise HTTPException(status_code=400, detail='Invalid signature')
except ValueError:
raise HTTPException(status_code=400, detail='Invalid payload')
if event.is_successful():
# Process completed book
await process_book(event)
else:
# Handle failure
await handle_failure(event)
return {'received': True}
Django #
# views.py
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from nellie_api import Webhook, WebhookSignatureError
import os
WEBHOOK_SECRET = os.environ['NELLIE_WEBHOOK_SECRET']
@csrf_exempt
@require_POST
def nellie_webhook(request):
try:
event = Webhook.construct_event(
request.body,
request.headers.get('X-Nellie-Signature'),
WEBHOOK_SECRET
)
except WebhookSignatureError:
return HttpResponseBadRequest('Invalid signature')
except ValueError:
return HttpResponseBadRequest('Invalid payload')
if event.is_successful():
# Queue background task to download book
from .tasks import process_completed_book
process_completed_book.delay(event.request_id, event.result_url)
return JsonResponse({'received': True})
Express.js #
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.NELLIE_WEBHOOK_SECRET;
// Use raw body for signature verification
app.post('/webhooks/nellie',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString();
const sigHeader = req.headers['x-nellie-signature'];
if (!verifySignature(payload, sigHeader, WEBHOOK_SECRET)) {
return res.status(400).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
if (event.status === 'completed') {
// Process completed book
processBook(event.requestId, event.resultUrl);
} else {
// Handle failure
console.error(`Book failed: ${event.error}`);
}
res.json({ received: true });
}
);
Webhook URL Requirements #
Your webhook URL must:
| Requirement | Details |
|---|---|
| HTTPS | Must use HTTPS (not HTTP) |
| Public | Must be publicly accessible |
| Valid certificate | Must have a valid SSL certificate |
| Respond quickly | Should return 2xx within 30 seconds |
Not allowed:
localhostor127.0.0.1- Internal IP ranges (10.x.x.x, 192.168.x.x, etc.)
- Cloud metadata endpoints
- Non-HTTPS URLs
Retry Behavior #
If your endpoint doesn’t return a 2xx status code, Nellie will retry:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
After 3 failed retries, the webhook is abandoned. You can still poll the status endpoint to get results.
Testing Webhooks #
Local Development #
Use a tunnel service to expose your local server:
# Using ngrok
ngrok http 5000
# Gives you: https://abc123.ngrok.io
# Then use that URL
book = client.books.create(
prompt="Test book",
webhook_url="https://abc123.ngrok.io/webhooks/nellie"
)
Generate Test Signatures #
The SDK can generate test signatures:
from nellie_api import Webhook
import json
# Create test payload
payload = json.dumps({
"requestId": "test-123",
"status": "completed",
"completedAt": "2025-12-03T10:55:00Z",
"creditsUsed": 250,
"resultUrl": "https://example.com/test.pdf"
})
# Generate signature
signature = Webhook.generate_signature(payload, "whsec_your_test_secret")
# Use in tests
response = test_client.post(
'/webhooks/nellie',
data=payload,
headers={
'Content-Type': 'application/json',
'X-Nellie-Signature': signature
}
)
Best Practices #
✅ DO #
- Always verify signatures — Never process unverified webhooks
- Respond quickly — Return 200 immediately, process async
- Be idempotent — Handle duplicate deliveries gracefully
- Log everything — Keep records for debugging
- Use queues — Process heavy work in background jobs
- Skip verification — Anyone could POST to your endpoint
- Do heavy processing sync — Can cause timeouts
- Assume order — Webhooks may arrive out of order
- Ignore failures — Set up alerts for webhook errors
- Check URL is publicly accessible
- Verify HTTPS certificate is valid
- Check server logs for incoming requests
- Ensure firewall allows incoming connections
- Use the raw request body (not parsed JSON)
- Check webhook secret matches dashboard
- Verify timestamp is within 5 minutes
- Ensure correct header name:
X-Nellie-Signature - Return 200 immediately
- Process in background worker
- Don’t download files synchronously
❌ DON’T #
Troubleshooting #
Webhook Not Received #
Signature Verification Fails #
Timeouts #
Related Documentation #
- POST /v1/book — Set webhook_url parameter
- Authentication — Get your webhook secret
- SDK Reference — Webhook verification utilities
- Troubleshooting — Debug webhook issues