Skip to main content

Using Event Notifications

Complete this after installing the accelerator, configuring the Consent Portal, and assigning the dpdp-consent-admin role — see Install and Set Up and configuring-the-accelerator.md if you have not done that yet.

Event Notifications let an application publish an event to a topic and deliver that event to matching subscribers. Topics, subscriptions, events, and delivery history are isolated by Identity Server tenant.

1. Before you begin​

You need:

  • A running Identity Server with the DPDP Accelerator installed.
  • A user with the dpdp-consent-admin role in the tenant you want to manage.
  • An OAuth access token with the scopes required for any direct API calls.
  • A publicly reachable HTTPS endpoint if you use webhook delivery.

The automatically provisioned dpdp-consent-admin role contains the six Event Notification management scopes for topics, subscriptions, and events. The automatically provisioned dpdp-consent-user and dpdp-consent-dpo roles do not contain Event Notification scopes. Identity Server registers the polling and delivery-completion scopes, but these receiver scopes are not assigned to any of the three portal roles.

OperationRequired scopeAutomatically provisioned role
View topicsnotifications:topics:readdpdp-consent-admin
Create or deregister topicsnotifications:topics:writedpdp-consent-admin
View subscriptionsnotifications:subscriptions:readdpdp-consent-admin
Create, verify, or delete subscriptionsnotifications:subscriptions:writedpdp-consent-admin
View events and delivery historynotifications:events:readdpdp-consent-admin
Publish eventsnotifications:events:writedpdp-consent-admin
Poll event deliveriesnotifications:events:pollNone
Submit delivery completionnotifications:event-deliveries:completeNone

“None” means the scope exists in Identity Server but is not assigned to an automatically provisioned portal role. Assign receiver scopes explicitly to a dedicated application-to-application role when the integration requires them.

Configure publisher and receiver roles​

For application-to-application access, create separate least-privilege roles instead of assigning dpdp-consent-admin to publishing and receiving users:

RoleAssign these scopes
event-publishernotifications:events:write
event-receivernotifications:events:poll, notifications:event-deliveries:complete

Create separate publisher and receiver users, assign only the corresponding role, and obtain fresh access tokens after role assignment. Use the publisher token for POST /events and the receiver token for POST /events/poll and POST /deliveries/{deliveryId}/completion. As negative checks, the receiver token must receive HTTP 403 when publishing, while the publisher token must receive HTTP 403 when polling or submitting completion.

The Identity Server provisioning flow also creates dpdp-consent-admin, dpdp-consent-user, and dpdp-consent-dpo. The administrator role receives the six topic, subscription, and event management scopes; the user and DPO roles do not receive Event Notification scopes. None of these roles receives notifications:events:poll or notifications:event-deliveries:complete. The event-publisher and event-receiver roles above are least-privilege recommendations that an operator must create and assign manually.

API base URL​

https://<host>:9443/api/dpdp/event-notifications/v1

The endpoints use the request's Carbon tenant context as the organization identifier. Do not send a tenant or organization ID in a request body to select another tenant; it will not replace the request's tenant context.

The examples below use these shell variables:

IS_BASE_URL="https://is.example.com:9443"
TENANT_DOMAIN="example.com"
API_BASE="${IS_BASE_URL}/api/dpdp/event-notifications/v1"
GROUP_ID="${TENANT_DOMAIN}"
ACCESS_TOKEN="<access-token>"
PUBLISHER_ACCESS_TOKEN="<event-publisher-access-token>"
RECEIVER_ACCESS_TOKEN="<event-receiver-access-token>"

The examples use certificates trusted by the client. For a local Identity Server using a self-signed certificate only, add -k to a curl command temporarily. Do not use -k for deployed environments; configure a trusted CA or pass it explicitly with --cacert instead.

2. Understand the notification flow​

Events reach the framework in two ways:

  • Automatic lifecycle publication: with lifecycle publication enabled (the default), the accelerator publishes an event when the corresponding consent or user lifecycle action occurs for one of the five predefined system topics.
  • Explicit API publication: an authorized publisher calls POST /events, normally for a user-created topic or another intentional publication.

The normal webhook flow is:

  1. The Data Fiduciary creates a topic.
  2. The Data Fiduciary registers a webhook subscription on behalf of the Data Processors for that topic.
  3. Identity Server verifies the subscriber's callback URL.
  4. A matching lifecycle action publishes the event automatically, or an Event Publisher (the Data Fiduciary or an external publisher) sends it explicitly with a group-id header.
  5. The accelerator matches the event to active subscriptions and sends a signed webhook request.
  6. An administrator can inspect the event and delivery history in the portal.
ResourcePurpose
TopicA named event category, such as consent-status-changed.
SubscriptionAssociates one or more topics with one shared purpose filter and delivery configuration.
EventThe payload published to a topic for a group.
DeliveryOne subscription-specific attempt to deliver an event.

3. Create and manage topics​

In the Consent Portal, open Event Notifications → Topics. From this page you can search and filter topics, register a topic, or deregister a user-created topic.

Automatically provisioned system topics​

When an ordinary WSO2 tenant is created, the accelerator creates these topics for that tenant:

TopicDescriptionAutomatic publication triggerHas a purpose?
consent.updateConsent update and state transition notifications.A consent is updated, or an authorization is approved or rejected.Yes
consent.revokeConsent revocation and withdrawal notifications.A consent or its authorization is revoked through a lifecycle path that invokes the DPDP consent listener.Yes
consent.expireConsent expiration notifications.The expiry reconciler records a due consent as expired.Yes
user.data.changeUser data modification and profile change notifications.Identity Server completes a user-claim update.No
user.account.deleteUser account deletion and right-to-be-forgotten notifications.Identity Server completes a user deletion.No

Automatic publication is enabled by default with publishing_enabled = true under [dpdp_accelerator.event_notifications.lifecycle_events]. The lifecycle action creates the Event Notification event automatically; an administrator or external publisher does not need to call POST /events for that occurrence. A delivery is created only for an active subscription whose tenant, group, topic, and purpose filter match the event.

These topics are marked as managed by the system. A portal administrator can use them for subscriptions and events but cannot deregister them. The portal disables their deregistration action, and the API rejects direct deletion attempts.

About the "Has a purpose?" column: a subscription's purpose filter (see §5) only works if the event actually carries a purpose. The 3 consent topics do. user.data.change and user.account.delete don't — a user isn't tied to one processing purpose the way a consent is. So use the all filter when subscribing to those 2 topics; specific/all_except will simply never match anything on them.

Topic provisioning is independent of Consent Portal auto-provisioning. The system-topic step does not create or assign any user, group, or role. Consent Portal auto-provisioning separately creates its application roles, but role membership remains a manual administrator action. Updating the tenant safely reconciles any missing system topic without creating duplicates. For carbon.super, the server startup observer invokes the same provisioning flow after startup. The five system topics are therefore also created for the super tenant when system_topics_auto_create_enabled is true.

Enabling user.data.change / user.account.delete​

With the shipped defaults, consent callbacks and expiry tracking are enabled through consent_history.enabled, and the expiry reconciler is enabled through consent_expiry.enabled. Keep lifecycle publishing enabled as well. These two user topics additionally need this setting in deployment.toml:

[[event_handler]]
name = "dpdpUserLifecycleEventHandler"
subscriptions = ["POST_DELETE_USER", "POST_SET_USER_CLAIMS"]

Already included if you installed with bin/configure.sh — nothing to do. If you manage deployment.toml yourself, add this block and restart the server. Without it, these 2 topics stay silent: no events ever publish to them.

Register a user topic​

To create a topic through the API:

curl --request POST "${API_BASE}/topics" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"name": "consent-status-changed",
"description": "Changes to the status of a consent"
}'

A newly created topic is active. Topic names must be unique within the tenant.

To list active topics:

curl "${API_BASE}/topics?status=active&limit=20&offset=0" \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

List responses use this pagination shape:

{
"items": [],
"total": 0
}

To deregister a user-created topic:

curl --request DELETE "${API_BASE}/topics/<topic-id>" \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

A topic cannot be deregistered while it has active subscriptions. System-created topics cannot be deregistered by users.

4. Prepare a webhook receiver​

When creating a webhook subscription, you specify a callbackUrl (e.g. https://subscriber.example.com/events, https://subscriber.example.com/api/v1/webhook, etc.). The URL path is completely user-configurable.

Identity Server sends all HTTP POST requests directly to this callbackUrl. The single endpoint handles two distinct phases:

  1. Verification Handshake: Sent upon subscription registration or retry to verify endpoint ownership.
  2. Event Delivery: Sent when events matching your subscription occur. Webhook receiver request routing flow for verification handshake and event delivery

Respond to verification​

Identity Server verifies a callback URL by sending an HTTP POST request to your callbackUrl with Content-Type: application/json:

{
"type": "subscription.verification",
"subscriptionId": "<subscription-id>",
"topics": ["consent.update", "consent.revoke"],
"challenge": "<one-time-challenge>"
}

The topics array contains the subscribed topic names for this subscription.

What your receiver must do:

  1. Check body.type: if it equals "subscription.verification", handle this as a verification challenge (do not process as an event delivery).
  2. Validate that body.topics matches the topics your receiver is configured to accept.
  3. Respond with HTTP 200 OK, Content-Type: text/plain, and the exact challenge string as the response body.

Surrounding whitespace is ignored, but additional characters or JSON formatting cause verification to fail. A failed attempt can be retried. The subscription becomes active once this check passes.

Receive and verify deliveries​

An event delivery is an HTTP POST with Content-Type: application/json and these headers:

HeaderDescription
Delivery-IdStable delivery identifier. Use it to reject duplicate processing.
event-signaturesha256=<hex HMAC-SHA256> computed using the subscription's shared secret.

With payload signing enabled (the default), the body contains only the compact tenant-signed JWS:

{
"signedPayload": "<base64url-header>.<base64url-claims>.<base64url-signature>"
}

The decoded JWS claims have the following shape:

{
"iss": "https://is.example.com:9443/t/example.com/oauth2/token",
"sub": "example-group",
"aud": "dpdp-event-notifications",
"iat": 1787651970,
"jti": "<delivery-id>",
"txn": "<event-id>",
"payloadHash": "<HMAC-SHA256 of the embedded payload>",
"payload": {
"deliveryId": "<delivery-id>",
"eventId": "<event-id>",
"subscriptionId": "<subscription-id>",
"orgId": "example.com",
"groupId": "example-group",
"topic": "consent-status-changed",
"eventPayload": {
"consentId": "c7c6b814-ef76-4eb4-9494-8af1b98a9ed1",
"status": "REVOKED"
}
}
}

First compute HMAC-SHA256 over the exact raw request-body bytes, using the shared secret as the key. Compare sha256=<computed lowercase hex value> with the event-signature header using a constant-time comparison. Do not reserialize the outer JSON before verification because whitespace and field order change the signature. Then verify the compact JWS with the certificate identified by its kid/x5t#S256 protected headers. The kid must match an entry from the trusted JWKS endpoint for the tenant, and iss must exactly match the issuer configured by Identity Server for that tenant. Maintain this issuer-to-JWKS mapping in trusted receiver configuration; never fetch keys from an arbitrary location derived from an unverified JWS. The super tenant normally uses /oauth2/token and /oauth2/jwks without /t/carbon.super.

Validate iss, sub, aud, iat, jti, and txn, and process the event from payload.eventPayload. The event payload is intentionally not repeated outside the JWS. Receivers that previously accepted a bare tenant domain in iss must be updated to accept the configured Identity Server issuer.

Restrict the accepted signing algorithm to RS256. Check that jti, the Delivery-Id header, and payload.deliveryId agree; likewise match txn to payload.eventId. Check the expected tenant, group, topic, and subscription before handing payload.eventPayload to the business application. Keep an operator-controlled signing-key cache and refresh it from the trusted tenant JWKS endpoint during key rotation; an unknown key must not bypass verification.

Acceptance, retries, and processing responsibilities​

Return a 2xx response only after durable acceptance, for example after committing the verified event to an inbox database or durable queue. The sender currently waits up to five seconds for the HTTP request. Perform long-running business work asynchronously after that commit.

Receiver outcomeHTTP responseWhat happens next
Verified event committed to the inbox202 or another 2xxSender records delivery as delivered; your worker processes the accepted event.
Same verified delivery already accepted2xxAcknowledge it again without repeating the business operation.
Invalid signature or unexpected tenant/subscription401 or 403Reject it; investigate the configuration or sender.
Storage unavailable before acceptance503Sender retries according to its configured retry policy.

Every non-2xx response, including 4xx, and transport failure follows the sender's retry policy. With max_retries = 5, there are at most six attempts. The current backoff is base_backoff_seconds × 3^(retry number - 1); worker scheduling can delay an attempt further. Exhausted deliveries become failed and require operator investigation. Do not expect a redirect or a 409 duplicate response to count as success.

Use a durable uniqueness constraint on Delivery-Id and retain deduplication records across process restarts and any permitted replay window. A timeout can occur after your inbox commit, and sender-side recovery can resend a delivery. The HTTP body may be signed again on retry, so deduplicate by delivery ID, not by the body bytes. Do not assume deliveries arrive in order.

A 2xx response proves acceptance, not completed business work. Your worker must retry or quarantine failures after acceptance, make its business effects idempotent, and retain processing outcomes. If you use completion evidence, submit it only after processing, using the separate completion API. The sender can briefly still have the delivery in flight after your HTTP response; wait for its delivered state before reporting completion. A completion report does not replace the original HTTP acknowledgement.

For production deployments, use HTTPS and a certificate trusted by Identity Server. HTTP callback URLs should be enabled only for controlled development environments through the Event Notification settings described in configuring-the-accelerator.md.

Run a sample reference listener​

To help you test immediately without writing custom receiver code, reference implementations are provided in both Python and Node.js. Both implementations use only built-in standard libraries—no package manager, virtual environment, or external dependencies (pip or npm) are required:

Both listeners automatically handle the verification handshake by echoing challenge with HTTP 200, verify incoming HMAC-SHA256 signatures when SHARED_SECRET is set, and log the complete formatted verification and event payloads directly to terminal stdout in real time.

From a repository checkout, their paths are:

  • docs/static/examples/webhook-listener.py
  • docs/static/examples/webhook-listener.mjs

Step-by-step setup:​

  1. Configure your shared secret and start the listener: Choose a shared secret (you will supply this same secret when registering the subscription in the Consent Portal):

    • Using Python:

      export SHARED_SECRET="$(openssl rand -hex 32)"
      echo "Your Shared Secret: ${SHARED_SECRET}"

      python3 docs/static/examples/webhook-listener.py
    • Using Node.js:

      export SHARED_SECRET="$(openssl rand -hex 32)"
      echo "Your Shared Secret: ${SHARED_SECRET}"

      node docs/static/examples/webhook-listener.mjs

    By default, the listener starts on http://127.0.0.1:8443/dpdp/events (customize PORT, HOST, or CALLBACK_PATH via environment variables if desired). Expose the endpoint at a reachable address matching your subscription's callbackUrl.

  2. Register the subscription: In the Consent Portal, register the subscription with your matching callbackUrl and the shared secret printed above. Identity Server immediately sends a verification request, which the listener logs and approves with HTTP 200.

  3. Observe live event deliveries in your terminal: Whenever an event occurs on your subscribed topics, Identity Server delivers the event to your callback URL. The listener verifies the HMAC-SHA256 signature, pretty-prints the decoded event payload directly to the console, and responds with HTTP 202 Accepted.

Production receivers should persist incoming events to a durable inbox or message queue, deduplicate by Delivery-Id, verify tenant JWS signatures against Identity Server's JWKS endpoint, and acknowledge HTTP 202 before delegating work to asynchronous business workers.

5. Register a subscription​

Register a webhook subscription​

In the Consent Portal, open Event Notifications → Subscriptions and select Register Subscription. Choose the subscribed topics (between 1 and 100 unique active topics), a purpose filter, webhook delivery, the callback URL, and a shared secret. Save the secret in the webhook receiver's secret store before submitting the form.

Purpose filters behave as follows:

FilterMatching events
allEvery event on the topic.
specificEvents containing at least one listed purpose.
all_exceptEvents containing at least one purpose that is not listed.

specific and all_except require at least one purpose. Purpose matching is case-insensitive. A subscription containing user.account.delete or user.data.change requires the all purpose filter. Topics are fixed at creation. A tenant and group cannot mix webhook and poll subscriptions for the same topic. Duplicate or overlapping webhook subscriptions using the same callback URL are also rejected. Referenced topics cannot be deregistered while any subscription referencing them is active, pending, or stale.

To register a webhook subscription through the API:

curl --request POST "${API_BASE}/subscriptions" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"topics": ["consent.update", "consent.revoke"],
"filter": {
"type": "specific",
"purposes": ["account-management"]
},
"delivery": {
"mode": "webhook",
"callbackUrl": "https://subscriber.example.com/dpdp/events",
"sharedSecret": "<strong-random-secret>"
}
}'

The subscription starts in pending. It becomes active only after the callback returns the verification challenge successfully. If verification is exhausted, the subscription becomes stale.

To retry verification after fixing the receiver:

curl --request POST "${API_BASE}/subscriptions/<subscription-id>/verify" \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

You can also open the subscription details in the portal and select Verify.

Register a poll subscription​

A poll subscription does not use a callback URL and becomes active immediately. It still requires a strong shared secret for request HMAC validation and outgoing event-envelope hashing. Store the secret securely before submitting the request because service responses do not return it.

curl --request POST "${API_BASE}/subscriptions" \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"topics": ["consent-status-changed"],
"filter": {
"type": "all",
"purposes": []
},
"delivery": {
"mode": "poll",
"sharedSecret": "<strong-random-secret>"
}
}'

Retain the returned subscriptionId and the supplied shared secret. Both are required when polling for deliveries.

6. Publish an event​

For tenants where the five predefined lifecycle topics exist, events are published automatically when their corresponding lifecycle actions occur and automatic publication is enabled. Use the API below for user-created topics or when an application intentionally publishes an event itself; do not duplicate an automatically generated lifecycle event by publishing it again manually.

Publish only to an active topic. The group-id header is required and controls which group-scoped subscriptions can receive the event. With subscriptions created by the current API, use the current tenant domain as the group ID.

curl --request POST "${API_BASE}/events" \
--header "Authorization: Bearer ${PUBLISHER_ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "group-id: ${TENANT_DOMAIN}" \
--data '{
"topic": "consent-status-changed",
"purposes": ["account-management"],
"payload": {
"consentId": "c7c6b814-ef76-4eb4-9494-8af1b98a9ed1",
"status": "REVOKED"
}
}'

The event and its matching delivery records are created atomically. Only active subscriptions on the same tenant, group, and topic whose purpose filter matches the event are selected.

7. Process deliveries​

How a consumer processes a delivery depends on the subscription's delivery mode. Webhook consumers receive pushed deliveries and may submit completion evidence. Poll consumers request pending deliveries and acknowledge them or report processing errors in a later poll request.

Submit webhook delivery completion​

Webhook consumers can submit a signed completion report to POST /deliveries/{deliveryId}/completion with the notifications:event-deliveries:complete scope. The request body is signed using the subscription shared secret and the event-signature: sha256=<hex> header. The signature input binds the completion to its path delivery identifier:

v1\ncompletion\n<deliveryId>\n<exact-request-body>

Clients must preserve the body bytes exactly after calculating the signature; changing whitespace or field order invalidates it. A signature generated for one delivery cannot be reused for another delivery. Body-only completion signatures are not accepted. The body contains completionStatus, completionEvidence, and an optional completedAt epoch-millisecond value. Completion is accepted only after the webhook delivery reaches delivered. completionEvidence must be an absolute HTTPS URL without credentials or a fragment and must not exceed 512 characters. A second completion for the same delivery returns EN-4090 with HTTP 409 Conflict.

An accepted completion returns HTTP 204 No Content with an empty response body. Unknown deliveries, group mismatches, and invalid signatures return the same 401 / EN-4010 response so an unauthenticated caller cannot discover a delivery or its state.

For example, a client can calculate and submit the contextual signature with:

DELIVERY_ID="<delivery-id>"
COMPLETION_SHARED_SECRET="<subscription-shared-secret>"
COMPLETION_BODY='{"completionStatus":"completed","completionEvidence":"https://processor.example/evidence/receipt.pdf"}'
COMPLETION_SIGNATURE=$(printf 'v1\ncompletion\n%s\n%s' "${DELIVERY_ID}" "${COMPLETION_BODY}" \
| openssl dgst -sha256 -hmac "${COMPLETION_SHARED_SECRET}" | awk '{print $2}')

curl -X POST "${API_BASE}/deliveries/${DELIVERY_ID}/completion" \
-H "Authorization: Bearer ${RECEIVER_ACCESS_TOKEN}" \
-H "group-id: ${GROUP_ID}" \
-H "event-signature: sha256=${COMPLETION_SIGNATURE}" \
-H "Content-Type: application/json" \
-d "${COMPLETION_BODY}"

For the full request/response schema, see the POST /deliveries/{deliveryId}/completion operation in the DPDP Event Notification API specification.

Poll event deliveries​

Polling uses short, stateless request-response semantics and is scoped to one subscription. A poll request may acknowledge previously received deliveries, report structured errors, and request pending deliveries in the same call. DPDP does not perform long polling, so returnImmediately=false is rejected.

The ack array contains delivery IDs, while the setErrs object is keyed by delivery ID; neither uses an event ID. Each delivery belongs to one subscription, so this prevents an acknowledgement for one subscription from updating another subscription's delivery of the same event. Only pending deliveries belonging to the request's tenant, group, and requested subscription are updated. A delivery ID must not appear in both ack and setErrs.

The first poll may have an empty request body. When request HMAC validation is enabled, calculate the signature over the exact bytes sent: an empty request is the zero-length byte sequence, not {}. The service verifies those bytes first and only then applies the same defaults as an empty JSON object. Whitespace-only bodies follow the same rule and must be signed with their exact whitespace.

Example:

POLL_SUBSCRIPTION_ID="<poll-subscription-id>"
POLL_SHARED_SECRET="<poll-subscription-shared-secret>"
POLL_BODY='{
"orgId": "example.com",
"maxEvents": 20,
"returnImmediately": true,
"ack": ["delivery-that-succeeded"],
"setErrs": {
"delivery-that-failed": {
"err": "authentication_failed",
"description": "Unable to authenticate the signed event"
}
}
}'
POLL_SIGNATURE="sha256=$(printf %s "${POLL_BODY}" | openssl dgst -sha256 -hmac "${POLL_SHARED_SECRET}" -hex | awk '{print $2}')"

curl -X POST "${API_BASE}/events/poll" \
-H "Authorization: Bearer ${RECEIVER_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "group-id: ${GROUP_ID}" \
-H "subscription-id: ${POLL_SUBSCRIPTION_ID}" \
-H "event-signature: ${POLL_SIGNATURE}" \
-d "${POLL_BODY}"

The response contains sets, keyed by deliveryId, and moreAvailable. Each SET value is a compact RS256 JWS built with the tenant signing key using the same event envelope as webhook delivery. Clients should retain the key and use it in a later poll request's ack or setErrs field. For example:

{
"moreAvailable": false,
"sets": {
"f5f37c64-6130-4d44-9463-136c8bca4278": "<compact-RS256-JWS>"
}
}

When there are no pending deliveries, the response is:

{
"moreAvailable": false,
"sets": {}
}

Polling defaults and request HMAC enforcement are configured in deployment.toml:

[dpdp_accelerator.event_notifications.polling]
default_return_immediately = true
default_max_events = 20
max_events_limit = 100
request_hmac_validation_enabled = false

When HMAC validation is enabled, event-signature is mandatory and is verified over the exact request body using the selected poll subscription's shared secret. Poll subscriptions always require a shared secret because the same secret is used to hash the outgoing event envelope before tenant-key signing. An omitted maxEvents uses the configured default; maxEvents=0 is an acknowledge-only request. Values above max_events_limit are rejected.

8. Inspect events and delivery history​

Open Event Notifications → Events in the portal to view published events. Select an event to inspect its payload and subscription-specific deliveries. Open a subscription to see deliveries associated with that subscription and their audit history.

The corresponding read APIs are:

OperationMethod and path
List eventsGET /events
Get an eventGET /events/{eventId}
List deliveries for an eventGET /events/{eventId}/deliveries
Get delivery historyGET /events/{deliveryId}/history
List deliveries for a subscriptionGET /subscriptions/{subscriptionId}/events
Get a subscription delivery historyGET /subscriptions/{subscriptionId}/events/{deliveryId}

For example:

curl "${API_BASE}/events/<event-id>/deliveries?limit=20&offset=0" \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

9. Delete a subscription​

Delete a subscription from its row or details page in the Consent Portal, or use:

curl --request DELETE "${API_BASE}/subscriptions/<subscription-id>" \
--header "Authorization: Bearer ${ACCESS_TOKEN}"

Deletion changes the subscription to deleted; it does not erase its existing event and delivery audit history.

10. Troubleshooting​

SymptomCheck
Event Notification pages are not visibleConfirm the user has dpdp-consent-admin in the current tenant, then sign in again to obtain a token with the Event Notification scopes.
A request returns 401 or 403Confirm the access token belongs to the URL's tenant and contains the operation's required scope.
No topics appear when registering a subscriptionCreate a topic and confirm it is active.
Subscription remains pending or becomes staleConfirm the callback is reachable from Identity Server and returns the exact verification challenge with HTTP 200. Then retry verification.
Subscription creation returns 409Check for an existing or overlapping subscription for the same topic and purposes, and delete the conflicting subscription if it is no longer needed.
A system topic exists but no automatic lifecycle events appearConfirm lifecycle_events.publishing_enabled = true. Consent callbacks and expiry tracking currently require consent_history.enabled = true; expiry reconciliation also requires consent_expiry.enabled = true. For user topics, confirm the dpdpUserLifecycleEventHandler subscriptions.
Event is created but no delivery appearsConfirm the subscription is active, the topic and purpose filters match, and group-id equals the subscription's group ID.
Webhook signature does not matchCompute the HMAC over the unmodified raw request body, not only the nested payload.