Flow Studio
Webhook URL Management
Creating, rotating, and deleting webhook URLs via the admin API — lifecycle operations and how URL changes affect running integrations.
Webhook URL Lifecycle
| Operation | API | Effect |
|---|---|---|
| Create | POST /api/webhooks | Generates a new URL and secret |
| Get URL | GET /api/webhooks/{id} | Returns URL (secret never returned after creation) |
| Rotate Secret | POST /api/webhooks/{id}/rotate | Generates new secret; old secret valid for 24h grace period |
| Deactivate | PATCH /api/webhooks/{id} (active: false) | Stops accepting requests — returns 410 Gone |
| Delete | DELETE /api/webhooks/{id} | Permanently removes URL and secret |
Create Response
POST /api/webhooks
{
"processId": "proc-orders-main",
"threadId": "main",
"description": "Order events from Shopify"
}
Response 201:
{
"webhookId": "wh-abc123",
"url": "https://api.bizfirstai.com/webhook/tenant-acme/proc-orders-main",
"secret": "whs_sk_live_Xy9mQ3...", ← shown ONCE, then hashed
"createdAt": "2026-05-25T10:00:00Z"
}
Secret storage: The secret is shown only once at creation time. Store it securely immediately — the platform only stores a bcrypt hash. If lost, rotate to get a new secret.
Secret Rotation with Grace Period
When rotating a secret, the old secret remains valid for 24 hours. This allows you to update the secret in the external system without any downtime:
- Call POST /api/webhooks/{id}/rotate → receive new secret
- Update the secret in your external system (Shopify, GitHub, etc.)
- Old secret still accepted for 24 hours in case some requests are already in-flight
- After 24 hours, old secret is invalidated automatically
WebhookUrlService (Backend)
// ProcessEngine/Webhooks/WebhookUrlService.cs
public class WebhookUrlService
{
public async Task<WebhookRegistration> CreateAsync(CreateWebhookRequest request)
{
var secret = _secretGenerator.Generate(32); // cryptographically random
var credentialId = await _credentialResolver.StoreSecretAsync(secret);
var registration = new WebhookRegistration
{
TenantId = request.TenantId,
ProcessId = request.ProcessId,
ThreadId = request.ThreadId,
CredentialId = credentialId,
IsActive = true
};
await _repo.CreateAsync(registration);
return registration with { PlaintextSecret = secret }; // returned ONCE
}
}