TurfAI Webhook Integration Guide
Synced from the source repositories. Do not edit by hand.
This guide explains how to integrate external applications with TurfAI workflows using webhooks.
Overview
Webhooks allow external applications (Google Forms, TypeForm, custom apps) to trigger TurfAI workflows automatically. When an external event occurs, a POST request to the webhook URL starts a workflow execution.
┌─────────────────┐ POST ┌─────────────────┐ Queue ┌─────────────────┐
│ External App │ ────────────> │ TurfAI DMS │ ────────────> │ Job Router │
│ (Form/App) │ + payload │ (Webhook) │ │ (Execution) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
│ Returns
▼
execution_id +
polling_tokenQuick Start
Step 1: Create a Workflow
First, create a workflow in the TurfAI UI or via API.
Step 2: Create a Webhook
curl -X POST https://apisandbox.turfai.in/api/webhooks \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"activity": 123,
"name": "Job Application Webhook"
}'Response:
{
"success": true,
"data": {
"id": 1,
"name": "Job Application Webhook",
"url": "https://apisandbox.turfai.in/api/webhooks/trigger/1",
"secret_key": "a1b2c3d4e5f6...64_char_hex_string...",
"active": true,
"trigger_count": 0
}
}Step 3: Configure External App
Copy the url and secret_key to your external application.
Step 4: Trigger the Workflow
curl -X POST https://apisandbox.turfai.in/api/webhooks/trigger/1 \
-H "X-Webhook-Secret: a1b2c3d4e5f6...your_secret..." \
-H "Content-Type: application/json" \
-d '{
"candidate_name": "John Doe",
"email": "john@example.com",
"resume_url": "https://drive.google.com/file/d/ABC123"
}'Response:
{
"success": true,
"execution_id": 456,
"message": "Workflow execution started",
"polling_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Step 5: Poll for Results
curl https://apisandbox.turfai.in/api/workflow-executions/456 \
-H "Authorization: Bearer YOUR_POLLING_TOKEN"API Reference
Base URL
| Environment | URL |
|---|---|
| Development | http://localhost:1338/api |
| Staging | https://apisandbox.turfai.in/api |
Create Webhook
Creates a webhook for a workflow.
Endpoint: POST /webhooks
Authentication: Required (JWT Bearer token)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
activity | number | Yes | Workflow/Activity ID |
name | string | No | Human-readable name |
event_type | string | No | Event type (e.g., "form_submission") |
max_file_size_mb | number | No | Max upload size per file (1-50, default: 10) |
allowed_mime_types | array | No | Allowed file types (default: common doc types) |
auto_enable_rag | boolean | No | Auto-index uploaded files for RAG (default: false) |
accept_files | boolean | No | Enable file uploads (default: true) |
Example:
curl -X POST https://apisandbox.turfai.in/api/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"activity": 123,
"name": "HR Form Webhook",
"event_type": "form_submission",
"max_file_size_mb": 25,
"auto_enable_rag": true
}'Response:
{
"success": true,
"data": {
"id": 1,
"name": "HR Form Webhook",
"url": "https://apisandbox.turfai.in/api/webhooks/trigger/1",
"secret_key": "64_character_hex_secret",
"active": true,
"event_type": "form_submission",
"trigger_count": 0,
"createdAt": "2024-11-24T10:00:00.000Z"
}
}Trigger Workflow (PUBLIC)
Triggers a workflow execution via webhook. No authentication required - uses secret validation.
Endpoint: POST /webhooks/trigger/:webhookId
Authentication: None (uses X-Webhook-Secret header)
Headers:
| Header | Required | Description |
|---|---|---|
X-Webhook-Secret | Yes | The webhook's secret_key |
Content-Type | Yes | application/json |
Request Body:
Any JSON object - becomes the workflow's inputs.
Example:
curl -X POST https://apisandbox.turfai.in/api/webhooks/trigger/1 \
-H "X-Webhook-Secret: your_64_char_secret" \
-H "Content-Type: application/json" \
-d '{
"candidate_name": "Jane Smith",
"email": "jane@example.com",
"role_id": "software_engineer",
"resume_file_id": "1ABC123xyz"
}'Success Response (200):
{
"success": true,
"execution_id": 456,
"message": "Workflow execution started",
"polling_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Error Responses:
| Status | Error | Description |
|---|---|---|
| 400 | File upload failed | File validation error (size, type, quota) |
| 401 | Missing X-Webhook-Secret header | Secret header not provided |
| 401 | Invalid webhook secret | Secret doesn't match |
| 403 | Webhook is inactive | Webhook has been disabled |
| 404 | Webhook not found | Invalid webhook ID |
Trigger with File Upload
Webhooks support multipart/form-data for uploading files directly. Files are:
- Uploaded to GCS as documents owned by the webhook creator
- Validated against size and type limits
- Counted against the webhook owner's storage quota
Example with Files:
curl -X POST https://apisandbox.turfai.in/api/webhooks/trigger/1 \
-H "X-Webhook-Secret: your_64_char_secret" \
-F "resume=@candidate_resume.pdf" \
-F "cover_letter=@cover_letter.docx" \
-F "applicant_name=Jane Smith" \
-F "email=jane@example.com"Response with Files:
{
"success": true,
"execution_id": 456,
"message": "Workflow execution started",
"polling_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"uploaded_documents": [
{
"field_name": "resume",
"document_id": 789,
"file_url": "gs://turfdms/uploads/default/42/1732444800000-candidate_resume.pdf",
"file_name": "candidate_resume.pdf",
"mime_type": "application/pdf",
"file_size": 245760
},
{
"field_name": "cover_letter",
"document_id": 790,
"file_url": "gs://turfdms/uploads/default/42/1732444800001-cover_letter.pdf",
"file_name": "cover_letter.docx",
"mime_type": "application/pdf",
"file_size": 52480
}
],
"documents_count": 2
}Accessing Files in Workflow:
The uploaded file information is injected into workflow inputs:
// Direct access by field name
inputs.resume_file_url // "gs://turfdms/uploads/..."
inputs.resume_document_id // 789
// Structured access
inputs._documents.resume // { field_name, document_id, file_url, ... }
inputs._uploaded_files // Array of all uploaded filesFile Upload Limits:
| Limit | Default | Configurable |
|---|---|---|
| Max file size | 10 MB | Per webhook (1-50 MB) |
| Allowed types | PDF, images, Office docs | Per webhook |
| Storage quota | User's limit | Per user |
Supported File Types (default):
application/pdfimage/jpeg,image/png,image/gif,image/webptext/plain,text/csv,application/jsonapplication/msword(doc)application/vnd.openxmlformats-officedocument.wordprocessingml.document(docx)application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
DOCX Auto-Conversion:
DOCX files are automatically converted to PDF for better processing compatibility.
Poll Execution Status
Check the status of a workflow execution.
Endpoint: GET /workflow-executions/:executionId
Authentication: Required (JWT or polling_token)
Response:
{
"data": {
"id": 456,
"status": "completed",
"inputs": { "candidate_name": "Jane Smith", ... },
"results": {
"classification": { "type": "resume", "confidence": 0.95 },
"extraction": { "name": "Jane Smith", "skills": [...] }
},
"started_at": "2024-11-24T10:00:00.000Z",
"completed_at": "2024-11-24T10:00:45.000Z"
}
}Status Values:
| Status | Description |
|---|---|
queued | Waiting to be processed |
running | Currently executing |
completed | Finished successfully |
failed | Execution failed (check error field) |
List Webhooks
List all webhooks owned by the authenticated user.
Endpoint: GET /webhooks
Authentication: Required
Response:
{
"success": true,
"data": [
{
"id": 1,
"name": "HR Form Webhook",
"url": "https://apisandbox.turfai.in/api/webhooks/trigger/1",
"active": true,
"trigger_count": 42,
"last_triggered_at": "2024-11-24T09:30:00.000Z",
"activity": { "id": 123, "name": "Job Application Processing" }
}
]
}Update Webhook
Update webhook settings.
Endpoint: PUT /webhooks/:id
Authentication: Required (must own webhook)
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | New name |
active | boolean | Enable/disable webhook |
event_type | string | Event type |
Example:
curl -X PUT https://apisandbox.turfai.in/api/webhooks/1 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "active": false }'Delete Webhook
Permanently delete a webhook.
Endpoint: DELETE /webhooks/:id
Authentication: Required (must own webhook)
curl -X DELETE https://apisandbox.turfai.in/api/webhooks/1 \
-H "Authorization: Bearer $TOKEN"Regenerate Secret
Generate a new secret key (invalidates old secret immediately).
Endpoint: POST /webhooks/:id/regenerate-secret
Authentication: Required (must own webhook)
curl -X POST https://apisandbox.turfai.in/api/webhooks/1/regenerate-secret \
-H "Authorization: Bearer $TOKEN"Response:
{
"success": true,
"data": {
"id": 1,
"secret_key": "new_64_character_hex_secret"
}
}Integration Examples
Google Forms
- Create a Google Form
- Go to Extensions > Apps Script
- Add this code:
function onFormSubmit(e) {
const WEBHOOK_URL = 'https://apisandbox.turfai.in/api/webhooks/trigger/YOUR_ID';
const WEBHOOK_SECRET = 'your_secret_key';
const formResponse = e.response;
const itemResponses = formResponse.getItemResponses();
// Build payload from form responses
const payload = {};
itemResponses.forEach(item => {
payload[item.getItem().getTitle()] = item.getResponse();
});
// Add metadata
payload.submitted_at = new Date().toISOString();
payload.form_id = e.source.getId();
// Send to TurfAI
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Secret': WEBHOOK_SECRET
},
payload: JSON.stringify(payload)
};
try {
const response = UrlFetchApp.fetch(WEBHOOK_URL, options);
console.log('Webhook response:', response.getContentText());
} catch (error) {
console.error('Webhook failed:', error);
}
}- Set up trigger: Triggers > Add Trigger > onFormSubmit > From form > On form submit
TypeForm
- Go to Connect > Webhooks
- Add webhook URL:
https://apisandbox.turfai.in/api/webhooks/trigger/YOUR_ID - Use a middleware service (Zapier/n8n) to add the
X-Webhook-Secretheader
Or use TypeForm's API with a custom integration:
// Using n8n or custom middleware
const payload = {
...typeformData.form_response.answers,
submitted_at: typeformData.form_response.submitted_at
};
fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Secret': WEBHOOK_SECRET
},
body: JSON.stringify(payload)
});Node.js Application
const axios = require('axios');
async function triggerWorkflow(formData) {
const WEBHOOK_URL = process.env.TURFAI_WEBHOOK_URL;
const WEBHOOK_SECRET = process.env.TURFAI_WEBHOOK_SECRET;
try {
const response = await axios.post(WEBHOOK_URL, formData, {
headers: {
'Content-Type': 'application/json',
'X-Webhook-Secret': WEBHOOK_SECRET
}
});
console.log('Execution started:', response.data.execution_id);
// Optional: Poll for results
return await pollExecution(response.data.execution_id, response.data.polling_token);
} catch (error) {
console.error('Webhook failed:', error.response?.data || error.message);
throw error;
}
}
async function pollExecution(executionId, pollingToken, maxAttempts = 30) {
const BASE_URL = 'https://apisandbox.turfai.in/api';
for (let i = 0; i < maxAttempts; i++) {
const response = await axios.get(`${BASE_URL}/workflow-executions/${executionId}`, {
headers: { 'Authorization': `Bearer ${pollingToken}` }
});
const { status, results, error } = response.data.data;
if (status === 'completed') {
return results;
}
if (status === 'failed') {
throw new Error(`Workflow failed: ${error}`);
}
// Wait 2 seconds before next poll
await new Promise(r => setTimeout(r, 2000));
}
throw new Error('Polling timeout');
}Python Application
import requests
import time
import os
WEBHOOK_URL = os.getenv('TURFAI_WEBHOOK_URL')
WEBHOOK_SECRET = os.getenv('TURFAI_WEBHOOK_SECRET')
BASE_URL = 'https://apisandbox.turfai.in/api'
def trigger_workflow(form_data: dict) -> dict:
"""Trigger a TurfAI workflow via webhook."""
response = requests.post(
WEBHOOK_URL,
json=form_data,
headers={
'Content-Type': 'application/json',
'X-Webhook-Secret': WEBHOOK_SECRET
}
)
response.raise_for_status()
return response.json()
def poll_execution(execution_id: int, polling_token: str, max_attempts: int = 30) -> dict:
"""Poll for workflow execution results."""
for _ in range(max_attempts):
response = requests.get(
f'{BASE_URL}/workflow-executions/{execution_id}',
headers={'Authorization': f'Bearer {polling_token}'}
)
response.raise_for_status()
data = response.json()['data']
status = data.get('status')
if status == 'completed':
return data.get('results')
if status == 'failed':
raise Exception(f"Workflow failed: {data.get('error')}")
time.sleep(2)
raise Exception('Polling timeout')
# Usage
result = trigger_workflow({
'candidate_name': 'John Doe',
'email': 'john@example.com',
'resume_file_id': '1ABC123xyz'
})
execution_id = result['execution_id']
polling_token = result['polling_token']
results = poll_execution(execution_id, polling_token)
print('Workflow results:', results)Security
Secret Validation
- Secrets are 64-character hex strings (256 bits of entropy)
- Validation uses timing-safe comparison to prevent timing attacks
- Always store secrets securely (environment variables, secret managers)
Best Practices
- Never expose secrets in client-side code
- Use HTTPS for all webhook calls
- Rotate secrets periodically using the regenerate endpoint
- Disable unused webhooks rather than deleting them
- Monitor trigger_count for unusual activity
Polling Token
- The
polling_tokenreturned by trigger is a short-lived JWT (1 hour) - Use it only for polling execution status
- Don't store it long-term
Error Handling
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request (invalid payload) |
| 401 | Unauthorized (missing/invalid secret) |
| 403 | Forbidden (webhook inactive) |
| 404 | Not found (invalid webhook ID) |
| 500 | Server error |
Retry Strategy
async function triggerWithRetry(payload, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await triggerWorkflow(payload);
} catch (error) {
if (error.response?.status >= 500 && attempt < maxRetries) {
// Server error - retry with exponential backoff
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
throw error; // Don't retry 4xx errors
}
}
}Rate Limits
| Limit | Value |
|---|---|
| Requests per webhook | 100/minute |
| Concurrent executions | 10 per user |
| Payload size | 1 MB |
| Polling requests | 60/minute |
Troubleshooting
Webhook Not Triggering
- Check webhook is
active: true - Verify secret matches exactly (case-sensitive)
- Ensure
Content-Type: application/jsonheader is set - Check DMS logs:
gcloud run logs read turfai-dms --limit=50
Execution Stuck in "queued"
- Verify Job Router is running
- Check Redis connection
- Look for errors in processor logs
401 Unauthorized
- Missing
X-Webhook-Secretheader - Secret mismatch - regenerate and update external app
See Also
- Task Types Reference - All available workflow task types
- PLAN_2024_11_24.md - Implementation details