Skip to main content

Inbound Webhook Processor

The inbound webhook processor is a generic, multi-tenant system for receiving and acting on HTTP webhook events from external providers. A single receiver endpoint handles all providers and tenants. Each registered webhook carries its own signature validation rules and processing logic, eliminating the need for provider-specific code or configuration files.

Architecture

The system separates receiving from processing. The receiver returns a 200 to the external provider immediately; a background job performs the heavier work asynchronously.

Receiver — Authenticates the incoming request by validating the signature according to the webhook's configured scheme, then enqueues a background job and returns immediately. Target latency is under 200 ms.

Processor — Picks up the background job, loads the full webhook configuration, evaluates filter rules against the payload, and executes matching actions. All results are written to the event log.

Data model

The processor uses three views:

ViewPurpose
fnd_inbound_webhook_configurationsOne row per registered webhook — identity, signature, rules
fnd_inbound_webhook_logsRequest/response log for every received event
fnd_inbound_webhook_subscription_logsHistory of subscribe, unsubscribe, and resubscribe attempts

Configuring a webhook

All per-webhook configuration is defined when you register a new webhook in fnd_inbound_webhook_configurations. A single row is all that is needed to register a new webhook endpoint — no code deployment or infrastructure change is required.

Core fields

FieldPurpose
tenant_idTenant the webhook belongs to
webhook_idCombined with tenant_id to form the URL path
source_typeProvider category (e.g. DSM_ACCESS_APPLICATION, XRM_MERCHANT, GITLAB)
source_record_idThe domain entity this webhook is linked to
program_codeIdentifies the processing program used when handling the event
activeSet to FALSE to pause processing without deleting the configuration
statusCurrent lifecycle state (see Subscription lifecycle)

Signature validation fields

FieldPurpose
signature_check_enabledWhether to reject requests that fail signature validation
signature_schemeOne of NONE, TOKEN_HEADER, or HMAC_SHA256
signature_header_nameThe HTTP header that carries the signature or token
signature_credentialsThe secret or token used for validation (e.g. {"secret": "..."})

Processing fields

FieldPurpose
processing_rulesFilter rulesets and actions to execute (see Processing rules)
field_mapping_schemaOptional payload field remapping applied before rules are evaluated

Subscription fields (optional)

Only relevant for webhooks that require active registration with the external provider.

FieldPurpose
subscription_credentialsProvider-specific credentials needed to subscribe
provider_subscription_idID returned by the provider after subscribing
subscribe_logHistory of subscription events (see Subscription log)

Signature validation

The receiver validates the incoming request according to signature_scheme before logging or enqueuing.

NONE

No validation is performed. The request is accepted as long as it reaches the correct (tenant_id, webhook_id) endpoint. Use for providers that do not support signatures, or during initial development.

TOKEN_HEADER

A static token is compared against the value of signature_header_name. If the header is missing or the values do not match, the request is rejected with 401.

signature_scheme: TOKEN_HEADER
signature_header_name: X-Gitlab-Token
signature_credentials: {"token": "s3cret-token-here"}

HMAC_SHA256

The receiver computes an HMAC-SHA256 digest over a concatenated string (typically timestamp + raw request body) and compares it to the value provided in signature_header_name. The exact concatenation format is configured in signature_credentials.

{
"secret": "shared-hmac-secret",
"format": "{timestamp}.{body}"
}

If the computed digest does not match, the request is rejected with 401.

Subscription lifecycle

Some external providers require explicit subscription calls before they start delivering events. The platform handles this automatically based on the webhook configuration.

Subscription statuses

A webhook that uses provider subscriptions moves through the following statuses:

StatusMeaning
PENDINGA subscribe request has been sent to the provider but no confirmation has been received yet.
SUBSCRIBEDThe provider confirmed the subscription. The webhook is actively receiving events.
UNSUBSCRIBEDThe subscription was cancelled successfully and the provider is no longer sending events.
UNSUBSCRIBE_FAILEDAn unsubscribe request was sent but the provider did not confirm cancellation. Manual follow-up may be needed.
ERRORThe subscribe attempt failed. The webhook is not receiving events. Check the subscription log for details.
ARCHIVEDThe webhook has been permanently retired. No further subscribe or unsubscribe actions will be attempted.

Receiver-only webhooks (e.g. providers that use a push model where you register the URL in their dashboard) skip subscription entirely. These are created directly with status = SUBSCRIBED.

State transitions

Subscribing

Registering a webhook with subscription_credentials set automatically triggers a subscribe request to the external provider. While the request is in progress, the status is PENDING. Once the provider confirms, the status moves to SUBSCRIBED and the provider_subscription_id is recorded.

If the subscribe attempt fails — for example, because the provider credentials are invalid or the provider is unreachable — the status moves to ERROR. No events will be delivered while the webhook is in this state.

Unsubscribing

Setting active = FALSE on a subscribed webhook triggers an unsubscribe request. The platform calls the provider to cancel the subscription. If the provider confirms, the status moves to UNSUBSCRIBED. If the provider does not confirm, the status moves to UNSUBSCRIBE_FAILED, indicating that the provider may still be sending events even though the webhook is deactivated locally. You should verify directly with the provider that the subscription was removed.

Resubscribing

A webhook in ERROR, UNSUBSCRIBED, or UNSUBSCRIBE_FAILED status can be resubscribed. Setting active = TRUE (or explicitly requesting a resubscribe) sends a new subscribe request to the provider and returns the status to PENDING. This is useful when a previous subscription expired, was cancelled, or failed due to a transient error.

Resubscribing creates a new subscription with the provider. A new provider_subscription_id is recorded on success.

Archiving

When a webhook is no longer needed, it can be archived. This moves the status to ARCHIVED permanently. No further subscribe or unsubscribe actions will be attempted, and the configuration is retained for audit purposes.

Subscription log

Each subscription event is recorded in the webhook's subscription log, providing a full history of subscribe, unsubscribe, and resubscribe activity.

Each log entry includes:

FieldDescription
Event typeThe action that was attempted: SUBSCRIBE, UNSUBSCRIBE, or RESUBSCRIBE
StatusThe outcome of the attempt: PENDING, SUCCESS, or FAILURE
TimestampWhen the event occurred
DetailsAdditional context such as provider response messages or error descriptions

You can review the subscription log to diagnose connection issues, verify that a subscription was established, or confirm that an unsubscribe completed successfully. For example, if a webhook is in ERROR status, the most recent log entry will contain the reason the subscribe attempt failed.

Processing rules

processing_rules is an array of rulesets. Each ruleset has a filter (which events to act on) and a list of actions (what to do).

Filter rules

Filters use JSONPath expressions evaluated against the incoming payload. Multiple conditions can be combined with AND or OR logic.

{
"filter": {
"operator": "AND",
"conditions": [
{ "path": "$.event_type", "op": "eq", "value": "PAYMENT_RECEIVED" },
{ "path": "$.amount", "op": "gt", "value": 0 }
]
},
"actions": [...]
}
OperatorMeaning
eqEquals
neNot equals
gtGreater than
ltLess than
containsString or array contains value
existsPath resolves to a non-null value

If no filter is provided, the ruleset matches every event.

Actions

Each action in the actions array describes an HTTP call the processor will make when the ruleset's filter matches.

{
"type": "HTTP_POST",
"url_template": "https://api.internal/v1/tenants/{{tenant_id}}/payments",
"headers": {
"Authorization": "Bearer {{cognito_token}}"
},
"body_template": "{\"external_id\": \"{{$.payment_id}}\", \"amount\": {{$.amount}}}"
}

Templates use Airspeed Velocity syntax. Variables available in templates include fields from the incoming payload (using JSONPath notation) and platform context values such as tenant_id and cognito_token.

Action typeDescription
HTTP_GETGET request to the resolved URL
HTTP_POSTPOST request with rendered body
HTTP_PUTPUT request with rendered body

Duplicate detection

Before executing actions, the processor checks whether a previous event with the same payload has already been processed. If a match is found, the event is skipped and logged as a duplicate. This provides at-least-once delivery semantics with idempotent deduplication.

Example: registering a new webhook

The following example configures a webhook for an external payment provider that signs requests with HMAC-SHA256 and sends a PAYMENT_RECEIVED event. On receipt, the processor should call an internal endpoint to create a cash receipt.

1. Register the webhook

Create a new webhook configuration with the following settings:

{
"tenant_id": "9f3a1b2c-4d5e-6f78-90ab-cdef12345678",
"source_type": "XRM_MERCHANT",
"source_record_id": "merchant-uuid-here",
"program_code": "process_inbound_webhook",
"signature_check_enabled": true,
"signature_scheme": "HMAC_SHA256",
"signature_header_name": "X-Acme-Signature",
"signature_credentials": {
"secret": "my-shared-secret",
"format": "{timestamp}.{body}"
},
"processing_rules": [
{
"filter": {
"operator": "AND",
"conditions": [
{ "path": "$.event", "op": "eq", "value": "PAYMENT_RECEIVED" }
]
},
"actions": [
{
"type": "HTTP_POST",
"url_template": "https://api.internal/v1/tenants/{{tenant_id}}/far/cash-receipts",
"headers": { "Authorization": "Bearer {{cognito_token}}" },
"body_template": "{\"external_ref\": \"{{$.payment_id}}\", \"amount\": {{$.amount}}}"
}
]
}
],
"status": "SUBSCRIBED",
"active": true
}

Because this provider does not require an explicit subscription call, subscription_credentials is left empty and status is set directly to SUBSCRIBED.

2. The provider sends an event

The provider POSTs to /persist/v2/webhook_in/{tenant_id}/{webhook_id}. The receiver:

  1. Loads the configuration for the (tenant_id, webhook_id) endpoint.
  2. Validates the X-Acme-Signature header using HMAC-SHA256.
  3. Logs the event as RECEIVED.
  4. Enqueues a background job for processing.
  5. Returns 200 to the provider.

3. The processor handles the event

The background job is picked up by the processor:

  1. Loads the full webhook configuration.
  2. Evaluates the filter: $.event == "PAYMENT_RECEIVED" — matches.
  3. Checks for a duplicate — none found.
  4. Renders the HTTP_POST action template and calls the internal cash-receipt endpoint.
  5. Logs the event as PROCESSED with the action result.
  6. Marks the job as COMPLETED.