[Discussion] Platform API Webhook Event Handling #2315
DinithHerath
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Problem Statement
The Platform API currently has no dedicated path for handling webhook events emitted by other platform components when they perform an operation asynchronously. When a component such as Developer Portal or Management Portal performs an operation, the API Platform needs to become aware of that operation later and update its own state accordingly.
Examples:
The original proposal[1] placed this webhook-handling responsibility on the Gateway Controller. That design assumed Gateway holds and serves the authoritative API-key / subscription state directly to the policy engine, and that Gateway should be reachable by Developer Portal.
In our deployment topology we do not ship a combined Developer Portal + Gateway solution. Platform API is the control plane that already owns API-key and subscription state and already synchronizes that state to all Gateway replicas through an existing EventHub + WebSocket mechanism. Pointing Developer Portal at every Gateway and re-implementing persistence, signature verification, idempotency, and replica fan-out inside the Gateway would duplicate logic that Platform API already provides.
This revised proposal therefore implements the webhook endpoint in Platform API instead of the Gateway. Gateway state is updated as a downstream effect of Platform API's normal control-plane synchronization, with no Gateway-side webhook endpoint required.
Relationship to the Developer Portal webhook design
This document is the receiver-side counterpart to the Developer Portal producer-side design in [2]. In that design the Developer Portal is the source of truth: it commits a domain change, writes an event to a transactional outbox, and asynchronously delivers signed webhooks to a control-plane webhook receiver per
gateway_type. The portal never talks to a gateway data plane directly.Platform API is the receiver registered for
gateway_type = "wso2/api-platform". Its job is to verify, decrypt, persist, and reconcile — and then propagate to its own gateway fleet using the control-plane sync it already owns. The contracts below (envelope, HMAC signing, hybrid encryption of secrets, at-least-once delivery) are fixed by the producer design; this document specifies how Platform API fulfills them.Scope
This effort focuses on the Platform API-side webhook implementation.
The first set of supported event families:
apikey.generatedapikey.regeneratedapikey.revokedsubscription.createdsubscription.plan_changedsubscription.deletedWhy Platform API instead of Gateway
gateway_eventsdelivery; no new fan-out pathThe net effect: the webhook handler only needs to validate the event and apply the domain change to Platform API storage. Propagation to Gateways is an existing, already-tested capability.
Proposal
GatewayEventsService(EventHub). Connected Gateway replicas receive it over their WebSocket control connection and update their own state and xDS snapshots — exactly as they do today forapi.deployed,apikey.*, andsubscription.*events.High-Level Flow
Webhook Endpoint
POST /api/internal/v1/webhook/eventswebhook.enabled = true).Configuration options
enabled- controls whether the webhook endpoint is registered.secret- the HMAC signing secret shared with the event producer (Developer Portal).private_key_path- points to the Platform API private key used to decrypt encrypted sensitive payload fields.gateway_type- used to filter events meant for this platform type.signature_tolerance- prevents replay attacks using old signed requests.max_body_size- protects the endpoint from unexpectedly large request bodies.Configuration is loaded via the existing koanf-based loader (
src/config/config.go) with env-var overrides (e.g.WEBHOOK_ENABLED,WEBHOOK_SECRET). Thesecretandprivate_key_pathshould be sourced from env/secret mounts in production rather than committed TOML.Event Envelope
All webhook events share a common envelope. It mirrors the Developer Portal
DP_EVENToutbox row (event_id,event_type,org_id,gateway_type,aggregate_type,aggregate_id,data/payload,occurred_at):{ "event_id": "550e8400-e29b-41d4-a716-446655440000", "event_type": "apikey.generated", "occurred_at": "2026-04-24T10:00:00.000Z", "org_id": "org-uuid", "gateway_type": "wso2/api-platform", "aggregate_type": "apikey", "aggregate_id": "domain-object-id", "schema_version": "1.0", "data": {} }event_id- unique per event; used for idempotency.org_id- resolves to the Platform API organization context (replaces the JWT-derived org context used by normal APIs).gateway_type- routing hint set by the producer. Platform API processes the event only when it matches the configuredgateway_type; otherwise the event is a no-op (202).aggregate_type- the kind of domain object affected (apikey,subscription), used to select the handler family alongsideevent_type.aggregate_id- stable domain object ID affected by the event. Included for events where the affected domain object must be identified even when event-specific fields are missing or change later.schema_version- envelope version for forward compatibility. This is forward-compat/optional: the Developer Portal lists it as planned work, so Platform API must tolerate its absence and treat a missing value as the current1.0schema rather than rejecting the event.Security Model
Webhook security has two separate responsibilities:
Signature Verification
secret.X-Devportal-Signature: t=1713952800,v1=<calculated_hmac>max_body_size).tandv1from the signature header.signature_tolerance).hmac.Equal/crypto/subtle).Sensitive Data Decryption
{ "encrypted_key": { "wrappedKey": "<base64>", // AES content key, RSA-OAEP-wrapped with Platform API's public key "iv": "<base64>", // AES-GCM nonce "tag": "<base64>", // AES-GCM auth tag "ciphertext": "<base64>" // AES-GCM-encrypted secret } }private_key_path):wrappedKeyto recover the one-time AES-256 content key.ciphertextwith that key,iv, andtag.src/internal/utils/subscription_token_crypto.gocovers stage 2 (the symmetric half) and the API-key hashing rules insrc/internal/service/apikey.gocover deriving the stored representation. The RSA-OAEP unwrap (stage 1) and PEM private-key loading are new code —crypto/rsa+crypto/x509— since the current utilities only handle a symmetric key derived from config, not an RSA-wrapped per-event key.HTTP Processing Flow
flowchart TD A[POST /api/internal/v1/webhook/events] --> B{Webhook enabled?} B -- No --> C[404] B -- Yes --> D[Read raw body with size limit] D --> E{Signature valid?} E -- No --> F[401 Unauthorized] E -- Yes --> G[Decode event envelope] G --> H{Envelope valid?} H -- No --> I[400 Bad Request] H -- Yes --> J{gateway_type matches?} J -- No --> K[202 Accepted as no-op] J -- Yes --> L{Dispatch by event_type} L --> M[Apply event-specific DB idempotency check] M --> N{Domain state already persisted?} N -- Yes --> O[Skip duplicate DB write] N -- No --> P[Persist domain state] O --> Q[Publish EventHub event via GatewayEventsService] P --> Q Q --> R{Publish successful?} R -- No --> S[500 Retryable failure] R -- Yes --> T[200 OK / 202 Accepted]Webhook vs Normal Platform API
org_idin the envelopeSuggested Package Structure
Mirrors the existing Platform API layout (handler → service → repository → model):
As built (see
platform-api-webhook-implementation.md§2 for the authoritative map):Handlers depend on the existing services/repositories rather than introducing a parallel persistence layer:
apikeyhandler →APIKeyRepository(api_keystable) +GatewayEventsService(apikey.*events).subscriptionhandler →SubscriptionRepository(subscriptionstable) +GatewayEventsService(subscription.*events).API Key Event Handling
Supported Events
apikey.generatedapikey.regeneratedapikey.revokedAPI Key Generation Event Payload
{ "event_id": "event-uuid", "event_type": "apikey.generated", "occurred_at": "2026-04-24T10:00:00.000Z", "org_id": "org-uuid", "gateway_type": "wso2/api-platform", "schema_version": "1.0", "aggregate_id": "key-uuid", "data": { "key_id": "key-uuid", "name": "my-key", "expires_at": "2027-01-01T00:00:00.000Z", "api": { "ref_id": "api-handle", "name": "Order API", "version": "1.0.0" }, "subscription": { "ref_id": "subscription-uuid", "plan_name": "Gold" }, "encrypted_key": { "wrappedKey": "<base64>", "iv": "<base64>", "tag": "<base64>", "ciphertext": "<base64>" } } }API Key Revoke / Regenerate Event Payload
{ "event_id": "event-uuid", "event_type": "apikey.revoked", "occurred_at": "2026-04-24T10:00:00.000Z", "org_id": "org-uuid", "gateway_type": "wso2/api-platform", "schema_version": "1.0", "aggregate_id": "key-uuid", "data": { "key_id": "key-uuid", "name": "my-key", "api": { "name": "MyFlights API", "version": "1.0.0", "ref_id": "api_handle" } } }(
apikey.regenerateduses the same shape.)data.key_idis mapped to an external reference on theapi_keysrecord, used to:apikey.generatedevent is received (idempotency);apikey.regeneratedorapikey.revokedevent is received.API Reference Resolution
data.api.ref_idresolves to the Platform API artifact / API record viaAPIRepository.org_idto locate the API-key record.API Key Generation Flow
data.api.ref_id.data.encrypted_key.api_key.hashing_algorithms).api_keystable (keyed bykey_id/ external ref + org).apikey.created(and analogous) event viaGatewayEventsService.API Key Regeneration / Revoke
The flow mirrors generation but uses
data.key_idto locate the exact API-key record, then upserts the new hash (regenerate) or marks itrevoked(revoke), and publishes the correspondingapikey.updated/apikey.revokedevent.Subscription Event Handling
Supported Events
subscription.createdsubscription.plan_changedsubscription.deletedSubscription Creation Event Payload
{ "event_id": "event-uuid", "event_type": "subscription.created", "occurred_at": "2026-04-24T10:00:00.000Z", "org_id": "org-uuid", "gateway_type": "wso2/api-platform", "schema_version": "1.0", "aggregate_id": "subscription-uuid", "data": { "subscription_plan": { "ref_id": "subscription_plan_name", "status": "ACTIVE" }, "api": { "ref_id": "api_handle", "name": "Order API", "version": "1.0.0" }, "encrypted_key": { "wrappedKey": "<base64>", "iv": "<base64>", "tag": "<base64>", "ciphertext": "<base64>" } } }Subscription Deletion / Plan Change Event Payload
{ "event_id": "event-uuid", "event_type": "subscription.deleted", "occurred_at": "2026-04-24T10:00:00.000Z", "org_id": "org-uuid", "gateway_type": "wso2/api-platform", "schema_version": "1.0", "aggregate_id": "subscription-uuid", "data": { "subscription_plan": { "ref_id": "subscription_plan_name", "plan_name": "Gold", "status": "ACTIVE" }, "api": { "name": "MyFlights API", "version": "1.0.0", "ref_id": "api_handle" } } }Subscription Reference Resolution
data.api.ref_idresolves to the Platform API API UUID.data.subscription_plan.ref_idresolves to the subscription-plan UUID for the givenorg_id(org + plan name is unique).subscriptionstable.Subscription Creation Flow
data.api.ref_id.data.subscription_plan.ref_id.data.encrypted_key.subscription_token_hashderivation).ACTIVE.subscription.createdevent viaGatewayEventsService.Subscription Plan Change / Delete
Same flow, using the resolved ref_ids to locate the subscription, then updating the plan (
subscription.updated) or marking it deleted/revoked (subscription.deleted), and publishing the corresponding event.Persistence and State Update Order
The webhook handler's responsibility ends at publishing through EventHub. Gateway-side convergence is decoupled and handled by the existing control-plane delivery, which already guarantees replica fan-out and reconnect/replay semantics.
Idempotency
event_idwill therefore be redelivered whenever Platform API does not return a success status (timeout,5xx, or a success that the producer never received). Platform API must handle duplicate deliveries safely — idempotency is mandatory, not optional.(api, name)already exists, or a subscription with the same API + subscriber already exists) and treats that as success. A duplicate delivery is therefore always a safe200.event_idledger to short-circuit exact replays before re-running the handler. That was dropped to minimize moving parts: it is an optimization (skip rework, suppress redundant gateway re-broadcasts), not a correctness requirement, and it could only ever be best-effort since the reused services commit the domain change and publish to the EventHub in separate transactions. The trade-off is that each duplicate re-runs the handler and re-broadcasts to gateways (same end state, redundant work). Reintroduce the ledger if duplicate-driven broadcast load becomes a concern. Seeplatform-api-webhook-implementation.md§5/§8.sequenceDiagram participant DP as Developer Portal participant API as Platform API participant DB as Platform API DB participant EH as EventHub (GatewayEventsService) DP->>API: POST event API->>API: Verify signature and validate envelope API->>DB: Check event-specific domain state alt State not found API->>DB: Persist domain state else State already exists API->>API: Skip duplicate DB write end API->>EH: Publish internal event alt Publish fails API-->>DP: 500 Retryable failure else Publish succeeds API-->>DP: 200 OK endEventHub & Replica / Gateway Synchronization
GatewayEventsService(src/internal/service/gateway_events.go).api.deployed,apikey.*, andsubscription.*events. Events are persisted to theeventstable and delivered to:Error Handling
404401401401400400400202200500500Summary of Changes from the Original Proposal([2])
POST /api/internal/v1/webhook/events).api_keys/subscriptionstables and repositories).GatewayEventsService, and the repository/handler/service layering already present in Platform API.[1] #2099
[2] #1774
Beta Was this translation helpful? Give feedback.
All reactions