Webhooks
Webhooks send real-time POST requests to your URL when events happen in your Flowxtra account — new applications, stage changes, hires, job updates, and meetings.
Setup
- Go to Plugins → Webhooks in your dashboard
- Click Add Webhook
- Enter a name, your endpoint URL, and select events
- Flowxtra generates an HMAC secret — copy it immediately (shown only once)
Endpoints
All webhook management endpoints require authentication and are tenant-scoped.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/plugins/webhooks/events | List available event types |
GET | /api/plugins/webhooks | List your webhooks |
POST | /api/plugins/webhooks | Create a webhook |
GET | /api/plugins/webhooks/{id} | Get webhook details + recent logs |
PUT | /api/plugins/webhooks/{id} | Update a webhook |
DELETE | /api/plugins/webhooks/{id} | Delete a webhook and its logs |
POST | /api/plugins/webhooks/{id}/test | Send a test payload |
GET | /api/plugins/webhooks/{id}/logs | Paginated delivery logs |
Create a Webhook
POST /api/plugins/webhooks?subdomain=acme
Request Body
{
"name": "My ATS Integration",
"url": "https://example.com/webhooks/flowxtra",
"events": [
"application.created",
"application.stage_changed",
"application.hired"
]
}
Response
{
"success": true,
"data": {
"id": 1,
"company_id": 1,
"name": "My ATS Integration",
"url": "https://example.com/webhooks/flowxtra",
"events": ["application.created", "application.stage_changed", "application.hired"],
"secret": "a1b2c3d4e5f6...full-64-char-secret",
"status": true,
"created_at": "2026-04-06T12:00:00.000000Z"
}
}
The secret is returned in full only on creation. Store it securely — subsequent responses mask it to ...last6chars.
Events
| Event | Trigger | Payload Fields |
|---|---|---|
application.created | New candidate applies | candidate_job_id, job_id, email, first_name, last_name, stage_id |
application.stage_changed | Candidate moves pipeline stage | candidate_job_id, job_id, new_stage_id, email |
application.rejected | Candidate rejected | candidate_job_id, job_id, email |
application.hired | Candidate hired as team member | candidate_job_id, job_id, team_member_id, email |
job.published | Job status set to Live | job_id, title, status |
job.closed | Job closed or archived | job_id, title, status |
meeting.scheduled | Meeting created (Zoom/Google) | meet_id, candidate_job_id, scheduled_at, type |
Payload Format
Every webhook delivery is a POST request with a JSON body:
{
"event": "application.created",
"timestamp": "2026-04-06T14:30:00+00:00",
"data": {
"candidate_job_id": 42,
"job_id": 7,
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"stage_id": 1
}
}
Request Headers
Each delivery includes these headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always JSON | application/json |
X-Flowxtra-Signature | HMAC SHA-256 signature | sha256=abc123... |
X-Flowxtra-Event | Event type | application.created |
X-Flowxtra-Delivery | Unique delivery ID (UUID) | 550e8400-e29b-... |
User-Agent | Identifies Flowxtra | Flowxtra-Webhooks/1.0 |
Verifying Signatures
Every delivery is signed with your webhook's HMAC secret. Always verify signatures to ensure the request is from Flowxtra.
Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your handler:
const isValid = verifySignature(
req.rawBody,
req.headers['x-flowxtra-signature'],
process.env.FLOWXTRA_WEBHOOK_SECRET
);
PHP
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_FLOWXTRA_SIGNATURE'];
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
Python
import hmac, hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Retry Policy
If your endpoint returns a non-2xx status code or the connection fails, Flowxtra retries up to 3 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 1 minute |
| 3rd retry | 5 minutes |
After 3 failed attempts, the delivery is marked as failed. Check Plugins → Webhooks → Logs to see delivery history and debug failures.
Best Practices
- Respond quickly — return
200 OKwithin 15 seconds. Process the data asynchronously if needed. - Verify signatures — always check
X-Flowxtra-Signaturebefore processing. - Handle duplicates — use
X-Flowxtra-Deliveryto deduplicate if your endpoint is called more than once. - Use HTTPS — always use an
https://endpoint URL. - Monitor logs — check delivery logs regularly to catch failures early.
Test Webhook
Send a test payload to verify your endpoint is working:
POST /api/plugins/webhooks/{id}/test?subdomain=acme
This sends a webhook.test event synchronously and returns the result immediately:
{
"success": true,
"data": {
"success": true,
"status_code": 200,
"message": "Test webhook delivered successfully."
}
}
Delivery Logs
View delivery history for a webhook:
GET /api/plugins/webhooks/{id}/logs?subdomain=acme
{
"success": true,
"data": {
"data": [
{
"id": 1,
"event": "application.created",
"status_code": 200,
"success": true,
"attempt": 1,
"delivered_at": "2026-04-06T14:30:01.000000Z",
"created_at": "2026-04-06T14:30:00.000000Z"
}
],
"current_page": 1,
"total": 1
}
}