Batch Notifications & Webhooks
Once a batch is submitted, you have a few options for tracking its completion: polling the API for status updates, or receiving a notification automatically when a batch completes. Enabling email notifications or webhooks means your application gets notified the moment a batch completes or fails, with no need to repeatedly check for updates.
Enabling email notifications
To receive emails when your batch has completed (either succesfully or otherwise), enable the option in your profile by sliding the toggle to on.
Webhooks
Webhooks let you receive an HTTP POST request to a URL of your choice when a batch completes or fails. To set one up:
- Go to your profile
- Enter your webhook URL
- Copy the signing key - you'll need this to verify that incoming requests are genuinely from Doubleword
Specification
Doubleword webhooks follow the Standard Webhooks specification. Every webhook request includes three headers:
webhook-id— a unique message identifierwebhook-timestamp— Unix timestamp of when the webhook was sentwebhook-signature— an HMAC-SHA256 signature in the formatv1,{base64-signature}
The signature is computed over {webhook-id}.{webhook-timestamp}.{body} using your signing key. Your signing key is prefixed with whsec_ — strip that prefix and base64-decode it to get the raw key. Always verify the signature before acting on the payload.
Your server must respond with a 2xx status code to acknowledge receipt. Any other status code (or a timeout) is treated as a failure.
Failed deliveries are retried with exponential backoff. After all attempts are exhausted, the delivery is marked as failed and no further retries are made. If your endpoint fails 10 consecutive deliveries, the webhook is automatically disabled to avoid repeatedly hitting a broken endpoint. You can re-enable it from your profile.
Event format
Webhooks are sent for two event types: batch.completed and batch.failed. The payload is a JSON object with the following structure:
{
"type": "batch.completed", // "batch.completed" or "batch.failed"
"timestamp": "2026-01-15T12:00:00Z", // when the event was emitted
"data": {
"batch_id": "batch_29cd13af-5ce9-487e-98ff-4eb6730c12b4",
"status": "completed", // "completed" or "failed"
"request_counts": {
"total": 100,
"completed": 98,
"failed": 2,
"cancelled": 0
},
// Download with the files API:
"output_file_id": "file_b4213e44-3c85-4689-9cae-e6d986785143",
"error_file_id": "file_49fdc045-77a8-409b-bb3e-d661df05f9fb",
"created_at": "2026-01-15T10:00:00Z",
"finished_at": "2026-01-15T12:00:00Z"
}
}Testing with Webhook.site
The quickest way to see webhooks in action is with Webhook.site:
- Visit webhook.site — you'll get a unique URL like
https://webhook.site/abc-123 - Copy the URL and paste it into the webhook URL field in your profile
- Submit a small batch and wait for it to complete
- You'll see the POST request appear on Webhook.site with the full payload and headers, including the
webhook-id,webhook-timestamp, andwebhook-signatureheaders
This is useful for inspecting the payload format, but doesn't verify the signature. For that, you'll need your own server.
Testing with a Python server
The following is a minimal Flask server that receives webhook requests and verifies the signature using your signing key.
import base64
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
# Paste your signing key from your Doubleword profile (starts with whsec_)
SIGNING_KEY = "whsec_your-signing-key-here"
def get_secret_bytes(key: str) -> bytes:
encoded = key.removeprefix("whsec_")
return base64.b64decode(encoded)
def verify_signature(msg_id: str, timestamp: str, body: bytes, signature: str) -> bool:
secret = get_secret_bytes(SIGNING_KEY)
signed_content = f"{msg_id}.{timestamp}.{body.decode()}"
expected = base64.b64encode(
hmac.new(secret, signed_content.encode(), hashlib.sha256).digest()
).decode()
expected_header = f"v1,{expected}"
return hmac.compare_digest(expected_header, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
msg_id = request.headers.get("webhook-id", "")
timestamp = request.headers.get("webhook-timestamp", "")
signature = request.headers.get("webhook-signature", "")
if not verify_signature(msg_id, timestamp, request.get_data(), signature):
abort(403)
event = request.get_json()
data = event["data"]
print(f"Batch {data['batch_id']} status: {data['status']}")
return "", 200
if __name__ == "__main__":
app.run(port=8000)To test this locally:
- Install Flask:
pip install flask - Run the server:
python webhook_server.py - Expose it to the internet with a tool like ngrok:
ngrok http 8000 - Copy the ngrok URL (e.g.
https://abc123.ngrok.io/webhook) into your Doubleword profile as the webhook URL - Submit a batch — when it completes, you should see the event logged and a
200response confirming the signature was valid