Skip to content

Webhook Receiver

Reference for the inbound webhook endpoint introduced in CFX-48. External e-commerce platforms send event notifications to this endpoint. Conflux validates the request signature, persists the call, then dispatches a queued job for platform-specific processing.

The receiver is built on spatie/laravel-webhook-client. Conflux supplies a custom signature validator (per-connection secret), a custom response, and a UUID-keyed webhook-call model; the package provides the controller, processing pipeline, and persistent buffer.

Endpoint

POST /api/webhooks/{connection}
Segment Type Description
connection UUID The id of the target SystemConnection

The route is defined in routes/api.php with ->withoutMiddleware(['auth:connection', 'ensure.connection.active']). There is no connection-guard authentication or EnsureSystemConnectionIsActive check. Authentication is handled exclusively via HMAC signature validation.

Route name: webhook-client-default — set by the package convention and required so the matching WebhookConfig (config/webhook-client.php) can be resolved from the current route name. It must not be renamed.

Authentication: HMAC-SHA256 Signature

Every request must include a Signature header containing the hex-encoded HMAC-SHA256 hash of the raw request body, using the connection's webhook_secret as the key.

Computing the signature

Signature: hex(HMAC-SHA256(raw_request_body, webhook_secret))

The webhook_secret is stored in the credentials encrypted JSON field of the SystemConnection model, under the key webhook_secret.

Example (PHP)

$signature = hash_hmac('sha256', $rawBody, $webhookSecret);
// Set as header: Signature: <value>

Example (shell)

echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}'

Request

  • Content-Type: application/json
  • Body: The raw JSON payload from the external platform. The structure is platform-specific and is passed through to the integration unchanged.

Required headers

Header Description
Signature Hex-encoded HMAC-SHA256 of the raw request body

Response

200 OK

Returned when the signature is valid, the call has been stored, and the job has been dispatched. The body is the package default, {"message":"ok"}.

The 200 response confirms that the payload has been accepted for asynchronous processing. It does not mean processing has completed — the payload is handled by ProcessWebhookJob via the queue.

Error responses

All error responses use the application/problem+json format defined in API Errors.

Status Condition Detail
403 Signature header is missing/invalid, or the connection has no webhook_secret configured The webhook signature is invalid.
404 No active SystemConnection with the given UUID exists (including soft-deleted or inactive connections) The requested resource was not found.

Signature failures funnel through the package's InvalidWebhookSignature exception, so a missing secret and a bad signature are intentionally indistinguishable to the caller. The 404 is produced by the connection route-model binding (see AppServiceProvider) before the receiver runs.

Processing pipeline

  1. Route-model binding (AppServiceProvider) resolves {connection} via SystemConnection::active()->findOrFail(). Unknown, inactive, or soft-deleted connections produce a 404 before any package code runs.
  2. ConnectionSignatureValidator validates the Signature header against the resolved connection's webhook_secret (HMAC-SHA256, hash_equals). Failure throws InvalidWebhookSignature403.
  3. The package's WebhookProcessor stores the call as a WebhookCall row, dispatches ProcessWebhookJob, and responds with the package default 200 OK ({"message":"ok"}).
  4. ProcessWebhookJob (extends the package job, receives the persisted WebhookCall) resolves the originating SystemConnection from connection_id, looks up the integration via IntegrationRegistry, and calls handleWebhook() with the stored payload. If the connection or its System no longer exists, the call is logged and discarded.
  5. AbstractIntegration::handleWebhook() (sealed as final) wraps processWebhook() with error handling. Any Throwable that is not an IntegrationRequestException is caught and re-thrown as InvalidIntegrationResponseException.
  6. processWebhook() is the method concrete integrations override to implement platform-specific webhook processing. The default implementation throws BadMethodCallException.

Persistence & retention

Every accepted call is stored in the webhook_calls table (UUID primary key, plus a connection_id linking back to the SystemConnection) for auditing and replay. This is a transient buffer: it is not soft-deleted and is mass-pruned after webhook-client.delete_after_days (30 days) via the scheduled model:prune command (routes/console.php).

Design decisions

Two deliberate choices deviate from defaults and are documented here for traceability:

  • webhook_calls has no soft deletes. The project default is to soft-delete most tables (Architecture → Soft Deletes). The webhook-call buffer is a sanctioned exception: it is transient, append-only audit/replay data that is hard-pruned on a schedule, so softDeletes() is intentionally omitted.
  • A missing secret returns the same 403 as a bad signature. When a connection has no webhook_secret, ConnectionSignatureValidator returns false rather than raising a distinct error, so the response is the generic The webhook signature is invalid. This keeps all signature-related failures behind a single InvalidWebhookSignature path and avoids leaking connection configuration state to unauthenticated callers.

Source files

File Purpose
routes/api.php Route definition (webhook-client-default) pointing at the package controller
config/webhook-client.php Receiver config: custom validator, response, model, and job
app/Providers/AppServiceProvider.php connection route-model binding (404 for unknown/inactive connections)
app/Services/Webhooks/ConnectionSignatureValidator.php HMAC validation against the per-connection webhook_secret
app/Models/WebhookCall.php UUID-keyed webhook-call buffer model (connection_id)
database/migrations/*_create_webhook_calls_table.php webhook_calls schema
bootstrap/app.php InvalidWebhookSignature403 exception handler
app/Jobs/ProcessWebhookJob.php Queued job: routes the stored payload through IntegrationRegistry to the correct integration
app/Services/AbstractIntegration.php handleWebhook() (final) and processWebhook() (overridable)
app/Contracts/IntegrationContract.php handleWebhook() contract method

See also