Sign in →
Protocols1 min read

Webhook Ingestion (Plugs)

Receive usage events from third-party providers — Stripe, Twilio, SendGrid, GitHub, Slack — without writing a single line of event-parsing code.

Updated 2026-06-15Suggest edits
Docs Ingestion Webhook Ingestion

Plugs Overview#

Plugs is Aforo's webhook receiver — a durable inbound endpoint that transforms raw third-party webhook payloads into normalized Aforo usage events. Instead of writing custom event-parsing code for every provider you integrate, you configure a Plug once in the Aforo UI and point the provider's webhook settings at Aforo.

Aforo handles the hard parts: HMAC signature verification, JSONPath field extraction, deduplication (using the provider's event ID), and delivery retries if your downstream billing pipeline is temporarily unavailable. Your engineering team touches nothing.

Zero parsing code
JSONPath mapping configuration replaces hand-written event parsers entirely
Tamper-proof delivery
Every inbound webhook is verified against the provider's HMAC signature before processing
Automatic deduplication
Provider event IDs are tracked for 72 hours; replays and retries are silently dropped
Audit trail
Every received, verified, and rejected webhook is logged with the raw payload for replay
INFO
Plugs routes to Aforo's usage-ingestor at POST /v1/ingest/webhook/{sourceId}. The sourceId is a stable UUID generated when you create the webhook source — it is the only routing identifier. No tenant ID or API key is required in the URL, because the source record is tenant-scoped server-side.

Creating a Source#

Navigate to Commercial → Webhook Sources → New Source in the Aforo admin. Each source represents a single provider integration. You can create multiple sources for the same provider — for example, one Stripe source per environment (production, staging).

Source Configuration Fields

FieldDescription
ProviderSelect from the built-in template list (Stripe, Twilio, etc.) or choose "Custom" for bespoke payloads.
Display NameHuman-readable label shown in the ingestion log and audit trail.
Metric IDThe Aforo billable unit that will receive the extracted usage quantity.
Signing SecretThe HMAC secret provided by the third-party provider. Stored encrypted with AES-256-GCM.
Event FilterOptional: a comma-separated list of event type strings. Payloads that do not match are dropped before extraction.
Field MappingsJSONPath expressions that extract quantity, tenant ID, timestamp, and custom properties from the payload.

After saving, Aforo generates a webhook URL unique to the source:

webhook-url.txt
https://api.aforo.ai/v1/ingest/webhook/wsrc_01HXKRM3ABCDE7F8G9HNPQRST

Paste this URL into the provider's webhook settings dashboard. Aforo begins receiving events immediately.

Signature Verification#

Every inbound request is verified before any payload processing occurs. An unverified or tampered webhook returns 401 Unauthorized and is logged — but never processed. The raw body is never parsed until signature validation passes.

Supported Signature Formats

FormatHeaderValue Example
StripeStripe-Signaturet=1713434800,v1=a3b4c5d6e7f8...
Bare hexX-Hub-Signature-256sha256=a3b4c5d6e7f8a1b2c3d4...
Raw SHA-256X-Signature-256a3b4c5d6e7f8a1b2c3d4e5f6...
SHA-1 legacyX-Hub-Signaturesha1=f1e2d3c4b5a6...
Custom header(configured per source)any format with configurable prefix

Verification uses MessageDigest.isEqual() — a constant-time byte comparison — to prevent timing attacks. A missing or empty signing secret returns 401 rather than a 500, so misconfigured sources fail closed and never leak config state to probing attackers.

WARNING
To test a new source without signature verification during development, set aforo.event-driven-sync.webhook.verify-signatures: false in your local config. Never disable signature verification in production. This flag is ignored unless the service is running with the dev Spring profile.

JSONPath Extraction#

Once a webhook passes signature verification, Aforo applies your JSONPath field mapping to extract the five standard usage event fields from the raw payload. Each mapping is a JSONPath expression that resolves to a scalar value within the provider's JSON body.

Standard Field Mappings

Aforo FieldTypePurpose
quantitynumber (required)The billable usage amount — call count, byte count, message count, etc.
tenant_idstring (required)Maps the event to a specific Aforo tenant. Often a customer or account identifier from the provider.
event_idstring (required)Provider-assigned unique event ID used for 72-hour deduplication.
timestampISO 8601 (optional)When the event occurred. Defaults to receipt time if absent. Rejected if >90 days in the past.
propertiesobject (optional)Arbitrary key-value pairs passed through to the usage event as custom dimensions.

Example: Mapping a Stripe Charge

A Stripe charge.succeeded webhook payload looks like this:

stripe-charge-payload.json
{
  "id":    "evt_1PH2KKLkdIwHu7ix9J0Pu7Ev",
  "type":  "charge.succeeded",
  "data": {
    "object": {
      "id":       "ch_3PH2KKLkdIwHu7ix0G7Xc2q9",
      "amount":   4900,
      "currency": "usd",
      "metadata": {
        "aforo_tenant_id": "tenant_acme_corp",
        "plan":            "professional"
      },
      "created":  1713434800
    }
  }
}

The corresponding JSONPath field mapping in Aforo:

stripe-source-mapping.json
{
  "event_filter": ["charge.succeeded"],
  "field_mappings": {
    "quantity":  "$.data.object.amount",
    "tenant_id": "$.data.object.metadata.aforo_tenant_id",
    "event_id":  "$.id",
    "timestamp": "$.data.object.created",
    "properties": {
      "currency": "$.data.object.currency",
      "plan":     "$.data.object.metadata.plan"
    }
  }
}
PRO TIP
The timestamp mapping accepts both ISO 8601 strings ("2026-04-18T09:22:00Z") and Unix epoch integers (1713434800). Aforo auto-detects the format.

Supported Providers#

Built-in provider templates pre-fill the field mappings and signature header configuration. Select a template when creating a source and adjust any fields that differ from the defaults.

ProviderSignature HeaderSignature FormatCommon Use Case
StripeStripe-Signaturet=…,v1=HMAC-SHA256Charge events → payment metering
TwilioX-Twilio-SignatureBase64 HMAC-SHA1SMS/voice events → message metering
SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA P-256Email events → message metering
GitHubX-Hub-Signature-256sha256=HMAC-SHA256Actions events → CI/CD metering
SlackX-Slack-Signaturev0=HMAC-SHA256App events → workspace usage metering
Custom(configurable)Any HMAC-SHA256 or SHA-1Internal services and bespoke providers

Sending a Test Webhook with curl

The example below simulates a Stripe charge.succeeded event being received by Aforo. Replace YOUR_SOURCE_ID with the ID from the source you created and compute the HMAC with your signing secret.

terminal
# Compute HMAC-SHA256 signature (Stripe format)
PAYLOAD='{"id":"evt_test_1","type":"charge.succeeded","data":{"object":{"id":"ch_test","amount":4900,"currency":"usd","metadata":{"aforo_tenant_id":"tenant_acme"},"created":1713434800}}}'
TIMESTAMP=$(date +%s)
SECRET="whsec_your_stripe_signing_secret"
SIG=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

# Send the webhook to Aforo
curl -X POST https://api.aforo.ai/v1/ingest/webhook/wsrc_YOUR_SOURCE_ID \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=${TIMESTAMP},v1=${SIG}" \
  -d "$PAYLOAD"

# Expected 200 response
{
  "status":   "accepted",
  "event_id": "evt_test_1",
  "metric_id": "stripe_charges",
  "quantity":  4900,
  "tenant_id": "tenant_acme"
}
INFO
Aforo returns 200 OK for unknown event types (types not in your event_filter list) without logging an error. This prevents the provider from retrying forever on events you deliberately ignore. Unknown event types are silently discarded after signature verification.