Skip to main content

Overview

Retab uses HTTPS to send webhook events to your app as a JSON payload. When a workflow completes, the End (Webhook) node sends the processed data to your configured endpoint.

Webhook Payload

The webhook sends a standardized JSON payload with the following structure:
FieldTypeDescription
documentMIMEData | nullDocument data (if a file was passed to the End node)
jsonobject | nullExtracted JSON data (if JSON was passed to the End node)

MIMEData Structure

The document field uses the MIMEData format:
FieldTypeDescription
filenamestringOriginal filename (e.g., "invoice.pdf", "scan.png")
urlstringBase64 data URL containing the file content
The url field is a data URL in the format:
data:<mime_type>;base64,<base64_encoded_content>
Example Payload:
{
  "document": {
    "filename": "invoice.pdf",
    "url": "data:application/pdf;base64,JVBERi0xLjQKJeLjz9..."
  },
  "json": {
    "invoice_number": "INV-2024-001",
    "vendor_name": "Acme Corp",
    "total_amount": 1250.00,
    "line_items": [
      { "description": "Widget A", "quantity": 10, "unit_price": 100.00 },
      { "description": "Widget B", "quantity": 5, "unit_price": 50.00 }
    ]
  }
}
Either document or json (or both) will be present depending on what’s connected to the End node. If only an Extract node is connected, you’ll receive json with the extracted data. If a document is also passed through, you’ll receive both.

Parsing the Payload

from pydantic import BaseModel, Field
from typing import Any
import base64

class MIMEData(BaseModel):
    """Document data in base64 data URL format."""
    filename: str = Field(description="Original filename of the document")
    url: str = Field(description="Base64 data URL of the file content")
    
    @property
    def mime_type(self) -> str:
        """Extract MIME type from the data URL."""
        if self.url.startswith("data:"):
            return self.url.split(";")[0].split(":")[1]
        return "application/octet-stream"
    
    @property
    def content(self) -> bytes:
        """Decode the base64 content."""
        if self.url.startswith("data:"):
            base64_data = self.url.split(",")[1]
            return base64.b64decode(base64_data)
        raise ValueError("URL is not a data URL")

class WebhookPayload(BaseModel):
    document: MIMEData | None = None
    json: dict[str, Any] | None = Field(default=None)

Setting Up Webhooks

To start receiving webhook events:
  1. Create a webhook endpoint handler to receive POST requests
  2. Configure the End node in your workflow with your webhook URL
  3. Test your endpoint locally using the Retab SDK
  4. Secure your webhook endpoint with signature verification

Create a Webhook Endpoint Handler

Set up an HTTPS endpoint that:
  • Handles POST requests with a JSON payload
  • Returns a successful status code (2xx) quickly before complex processing
  • Validates the webhook signature for security
import base64
from fastapi import FastAPI, Request
from pydantic import BaseModel
from typing import Any

app = FastAPI()

class Invoice(BaseModel):
    invoice_number: str
    vendor_name: str
    total_amount: float

@app.post("/webhook")
async def webhook_handler(request: Request):
    payload = await request.json()
    
    # Extract the JSON data
    json_data = payload.get("json")
    document = payload.get("document")
    
    if json_data:
        invoice = Invoice.model_validate(json_data)
        print(f"📬 Received invoice: {invoice.invoice_number} - ${invoice.total_amount}")
    
    if document:
        filename = document.get("filename")
        url = document.get("url", "")
        print(f"📎 Document: {filename}")
        
        # Optionally save the file
        if url.startswith("data:"):
            base64_content = url.split(",")[1]
            file_bytes = base64.b64decode(base64_content)
            with open(f"/tmp/{filename}", "wb") as f:
                f.write(file_bytes)
            print(f"   Saved to /tmp/{filename} ({len(file_bytes)} bytes)")
    
    return {"status": "success"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Configure the End Node

In your workflow:
  1. Add an End (Webhook) node
  2. Connect your Extract or Functions node to the End node
  3. Configure the webhook URL and any custom headers
{
  "webhook_url": "https://api.yourapp.com/workflow-results",
  "webhook_headers": {
    "Authorization": "Bearer your-api-key",
    "X-Workflow-ID": "wf_abc123"
  }
}

Test Your Webhook

Before deploying, test your webhook endpoint using the SDK:
from retab import Retab

client = Retab()

# Create a test processor and automation
processor = client.processors.create(
    name="Test Processor",
    model="gpt-4.1",
    json_schema=Invoice.model_json_schema(),
)

# Create an automation with your webhook
automation = client.processors.automations.links.create(
    name="Test Automation",
    processor_id=processor.id,
    webhook_url="https://your-server.com/webhook",
)

# Test with a document
result = client.processors.automations.links.test_document_upload(
    link_id=automation.id,
    document="test_invoice.pdf"
)

print(f"Test result: {result}")

Securing Your Webhook

An unsecured webhook endpoint is vulnerable to forged requests. Always verify the signature before processing webhook data.

How Signature Verification Works

Retab uses HMAC-SHA256 to sign webhook payloads:
  1. Signature Generation: Retab computes an HMAC-SHA256 signature of the payload using your webhook secret
  2. Signature Header: The signature is included in the Retab-Signature header
  3. Verification: Your server recomputes the signature and compares it to the header
If the signatures match, the request is authentic and unmodified.

Getting Your Webhook Secret

You can create a webhook security key directly from the End node in the workflow editor:
  1. Click the key icon (🔑) in the End node header
  2. Click Create Security Key if you don’t have one
  3. Copy the generated secret
  4. Store it securely as an environment variable (e.g., WEBHOOKS_SECRET)
Alternatively, you can manage keys in your Retab Dashboard under SettingsSecurity.

Implementation Examples

import os
from fastapi import FastAPI, Request, Response, HTTPException
from retab import Retab

client = Retab()
app = FastAPI()

@app.post("/webhook")
async def webhook_handler(request: Request):
    payload = await request.body()
    signature_header = request.headers.get("Retab-Signature")

    if not signature_header:
        raise HTTPException(status_code=400, detail="Missing Retab-Signature header")

    try:
        # Verify the signature
        client.verify_event(
            event_body=payload,
            event_signature=signature_header,
            secret=os.getenv("WEBHOOKS_SECRET"),
        )
    except Exception as e:
        return Response(
            status_code=400, 
            content=f"Signature verification failed: {str(e)}"
        )

    # Signature verified - process the webhook
    import json
    data = json.loads(payload.decode('utf-8'))
    json_data = data.get("json")
    document = data.get("document")
    
    # Process your data...
    
    return Response(status_code=200)

Security Best Practices

Never process webhook data without verifying the Retab-Signature header. This prevents attackers from sending forged requests to your endpoint.
Always use HTTPS endpoints for webhooks. HTTP endpoints expose your webhook data to interception.
Return a 2xx status code as soon as you receive and verify the webhook. Perform long-running processing asynchronously.
Retab may retry failed webhook deliveries. Implement idempotency to handle duplicate events gracefully.
Regularly rotate your webhook secret to minimize the impact of potential secret exposure.

Troubleshooting

Common Issues

IssueSolution
Missing signature headerEnsure you’re checking for Retab-Signature (case-insensitive)
Signature mismatchVerify you’re using the raw request body, not parsed JSON
Timeout errorsReturn 200 immediately, process data asynchronously
404 errorsCheck your webhook URL is accessible from the internet

Debugging Tips

  1. Log incoming requests: Temporarily log the raw payload and headers to verify what you’re receiving
  2. Test locally: Use a tool like ngrok to expose your local server for testing
  3. Check dashboard: View webhook delivery logs in your Retab dashboard
  4. Verify secret: Ensure your WEBHOOKS_SECRET matches the one in your dashboard