Appearance
WebhookReceiver — Spec
Generic inbound-webhook receiver. Its only job: authenticate the call, put it on that source's Service Bus queue, return 200. It has no DB, no Prime, and no knowledge of any payload. Consumers do the work.
Why separate from WebAPI
WebAPI deploys, restarts and slow downstream calls (Prime, OptimoRoute) must not make a sender see a 500 or a timeout. Senders retry non-2xx, and the receiver should depend on one thing only: Service Bus.
Endpoint
POST api/{source}/webhook — same URL shape as the WebAPI receivers (api/prime/webhook, api/freshdesk/webhook), so moving a sender is a host-name change only. GET /health is a liveness probe.
Per source, config holds:
| Key | Meaning |
|---|---|
Webhooks:<Source>:ApiKey | The sender's API-Key header value. Unset = every call 401 (fails closed) |
Webhooks:<Source>:Queue | Service Bus queue the message goes to |
An unconfigured source is 404. Adding a source is configuration only (plus its queue and a consumer).
Tenants. A sender with several instances posts to POST api/{source}/webhook/{tenant} instead. That route reads Webhooks:<Source>:Tenants:<Tenant>:ApiKey (its own key per tenant) and the tenant's own Queue if set, else the source's. An unknown tenant is 404. FreshDesk has two: api/freshdesk/webhook/it and api/freshdesk/webhook/claims. The bare api/freshdesk/webhook keeps working with the source's own ApiKey and sends no tenant; the consumer treats that as IT, so existing FreshDesk rules need no change until they are repointed.
Behaviour: wrong/missing key 401; body over 200 KB 413; Service Bus send failure 500 (the sender retries); otherwise 200.
Queue message
Subject = source. Body is a JSON envelope:
json
{ "source": "FreshDesk", "tenant": "claims" /* null on the bare URL */, "receivedUtc": "...", "query": { "event": "created" },
"headers": { "API-Key": "(redacted, 36 chars)", "...": "..." },
"remoteIp": "...", "payload": "<raw request body as a string>" }payload is the raw body string, so an unparseable body is still delivered. query carries FreshDesk's ?event=created|updated. Secrets (API-Key, Authorization) are replaced by their length. MessageId is a fresh GUID per receipt, so consumers own idempotency (FreshDesk/Prime retries produce duplicates).
Consumers
FreshDesk — WebAPI/Services/FreshDeskWebhookConsumer (a QueueConsumer) reads the envelope, takes the event from query.event, promotes the ticket id with the same logic as FreshDeskWebhookController, and inserts into ethFreshDeskWebhookLog (with tenant: Claims when the envelope tenant is claims, else It; migration 0059). Per tenant: It tickets feed IT provisioning (OnboardingProducer, created and updated); Claims tickets get the Prime job note (created only, via ClaimsFreshDeskClient, FreshDesk:Claims:Domain/:ApiKey). Each does nothing for the other tenant. Settings: FreshDesk:WebhookConsumerEnabled (default false) and ServiceBus:Queues:FreshDeskWebhook. A malformed envelope is dead-lettered; an insert failure retries (15 s, 45 s, 2 min, 5 min) then dead-letters. A crash between insert and settle can insert twice. Needs Listen and Send on the queue.
Prime — WebAPI/Services/PrimeWebhookConsumer does the same into ethPrimeWebhookLog, with PrimeWebhookController.ReadEvent (shared) promoting eventAction and the job GUID, and isAuthenticated = true. After the insert it nudges the n8n Prime Webhook Process Events flow (10 s cap; a failed nudge is only logged — the flow's schedule picks the row up). Settings: Prime:WebhookConsumerEnabled (default false) and ServiceBus:Queues:PrimeWebhook.
Migration state
The WebAPI receivers (PrimeWebhookController, FreshDeskWebhookController) are unchanged and still live, as a duplicate, so a sender can be pointed at either. Both queues have a consumer, but do not repoint a sender until that consumer's …:WebhookConsumerEnabled is on somewhere. Retire the WebAPI controllers once the senders have moved.
Deploying
New App Service, config keys above plus ServiceBus:Namespace (managed identity needs Azure Service Bus Data Sender on the queues) or ServiceBus:ConnectionString. Queue names in appsettings.json are placeholders — create the queues and confirm the names. No deploy workflow exists yet.

