ACCS Webhooks vs Events: Replacing Observers and Plugins

ACCS webhooks vs events banner: replacing observers and plugins in Adobe Commerce as a Cloud Service

If you’ve read my ACCS vs PaaS custom modules post, you know the bad news. On ACCS, you can’t drop an events.xml observer or a di.xml plugin into the codebase. So the real question becomes ACCS webhooks vs events: which one replaces the PHP hook you used to write? The short answer is that webhooks replace logic that must block or change a flow, and events replace logic that only reacts.

In my App Builder for ACCS walkthrough, I built a small event handler. This post goes one level up. I’ll compare the two models, map them to plugins and observers, and give you a decision table you can reuse. Every fact here comes from Adobe’s webhooks docs and events docs. The code is illustrative, so test it in a Stage workspace first.

ACCS webhooks vs events: the core difference

Both features start from the same place. Commerce hits a point in its code, like a product save or an add-to-cart. After that, they behave very differently.

A webhook is synchronous. Commerce sends an HTTP POST to your endpoint and waits for the answer. Your response can let the process continue, stop it with an error, or change the data. In other words, the shopper’s request is on hold until you reply.

An event is asynchronous. Commerce queues it and sends it to Adobe I/O Events, which then delivers it to your App Builder action or webhook URL. Nothing waits for you. The order is already saved by the time your code runs.

That single trait, blocking or not, drives almost every design choice below.

How they map to PHP plugins and observers

The naming gives you a big hint. Webhook methods take the form <event_type>.<webhook_name>, where the type is observer or plugin. Adobe’s own example is observer.sales_quote_add_item. Commerce events use a similar pattern, such as observer.catalog_product_save_after.

PHP extension pointClosest ACCS optionNotes
before pluginWebhook, type “before”Can validate input, reject with exception or adjust arguments.
after pluginWebhook, type “after”Can change the result, for example add a shipping method.
around pluginNo direct equivalentYou can’t wrap or skip the original call. Split it into before and after logic.
Observer that validatesWebhook on the observer.* methodUse only if you really must stop the flow.
Observer that syncs dataEvent on the observer.* eventThe natural fit. No checkout impact if your app fails.
Cron that polls for changesEvent, often with rulesPush beats polling, and rules cut the noise.

The “around” row is the one that hurts during migrations. I’d treat it as a design review, not a port.

Synchronous webhooks in detail

Response operations

Your endpoint must return HTTP 200 and JSON. According to the webhook responses page, that JSON is a single operation or an array of them. Five operations are supported.

  • success: the original process continues unchanged.
  • exception: Commerce stops the process. You can pass a message and an exception type.
  • add: adds a value at a path, optionally as a data object instance.
  • replace: swaps the value at a path.
  • remove: deletes the value or node at a path.

Timeouts, required hooks and fallbacks

Each hook has a hard timeout and a softTimeout, both in milliseconds. A request that passes the hard limit is aborted. One that passes the soft limit is only logged. Watch out, though: the default hard timeout is 0, which means no limit at all.

The required flag decides what happens on failure. If a required hook times out or returns a non-2xx code, Commerce logs an error and throws an exception. An optional hook just logs and moves on. The fallbackErrorMessage is what the shopper sees when things fail.

Hooks also live in batches. Batches run in order, but hooks inside one batch run in parallel. So never put a hook that depends on another hook’s answer in the same batch.

Signature verification

Webhook URLs are public, so you need to know the caller is really Commerce. Turn on signing under Stores > Settings > Configuration > Adobe Services > Webhooks and regenerate the key pair. Commerce then adds an x-adobe-commerce-webhook-signature header. Per the signature verification guide, it’s off by default, and regenerating the keys invalidates the old public key.

Asynchronous I/O Events in detail

Events take a longer path. Commerce sends them to an event provider in Adobe I/O Events. You then create an event registration in the Developer Console that routes events to an App Builder action, a webhook URL or the Journaling API.

Delivery isn’t instant. In the subscription docs, Adobe says priority events go out within a second, while non-priority ones can take up to 59 seconds. Cron must be enabled, too, because Commerce sends events through it.

Failures are handled at two levels. First, if Commerce can’t reach Adobe I/O, it retries up to seven times by default, according to the Commerce configuration page. Second, Adobe I/O Events retries delivery to your endpoint within a 24-hour window. If your endpoint keeps failing, the registration can be disabled. Even then, the I/O Events guide says events stay in the Journal for 7 days.

That journal is your safety net. You can replay missed events instead of guessing what you lost.

Finding available events and webhooks in ACCS

ACCS doesn’t support every event or webhook method that PaaS does. Instead, you get curated lists in the Admin.

  • Events: go to System > Events > Events List.
  • Webhooks: go to System > Webhooks > Webhooks List. Click a method to see its default payload.
  • Webhooks via REST: call GET /V1/webhooks/supportedList.

If the one you need is missing, Adobe’s advice is to open a support ticket. I’d check the list early, before you design anything around a hook that doesn’t exist.

Configuring them in ACCS

Webhook subscriptions

In the Admin, open System > Webhooks > Webhooks Subscriptions and click Add New Webhook. You pick the method, the type (before or after), batch and hook names, the URL and the timeouts. On ACCS, the URL must be a full URL, and you can set OAuth credentials right in the Admin. There’s also a REST endpoint if you’d rather script it. See Create a webhook for every field.

Then trim the payload with Hook Fields. Here’s the stock-check mapping from the hook contents docs, in the Admin format.

Webhook Method: observer.sales_quote_add_item
Hook Fields
  Name: product.sku       Source: data.quote_item.sku
  Name: product.quantity  Source: data.quote_item.qty
Timeout: 2000   Soft timeout: 200   Required: Yes
Fallback error message: Can't add the product to the cart right now

Event subscriptions

First, connect Commerce to your project under Stores > Settings > Configuration > Adobe Services > Adobe I/O Events. Paste the workspace JSON, set an instance ID, create the provider and enable eventing. I covered those steps in the App Builder post.

Next, add the subscription under System > Events > Events Subscriptions. List only the fields you need. If you leave fields empty, Commerce sends the full payload, which raises both cost and PCI risk.

Finally, route the event to your action. In App Builder, you can declare the registration in app.config.yaml, as the App Builder events guide describes. Treat the snippet below as a template and check the keys against that page.

application:
  runtimeManifest:
    packages:
      my-app:
        actions:
          product-sync:
            function: actions/product-sync/index.js
            web: 'no'
  events:
    registrations:
      Product saves:
        description: Product save events from ACCS
        events_of_interest:
          - provider_metadata: dx_commerce_events
            event_codes:
              - com.adobe.commerce.observer.catalog_product_save_after
        runtime_action: my-app/product-sync

Code samples

A webhook action that returns operations

This App Builder web action checks a cart line. It uses the raw-http annotation, so the body arrives base64 encoded in __ow_body. That matches Adobe’s signature example. The SKU rule is a made-up placeholder.

const crypto = require('crypto')

async function main (params) {
  const sig = params.__ow_headers['x-adobe-commerce-webhook-signature'] || ''
  const verifier = crypto.createVerify('SHA256')
  verifier.update(params.__ow_body)
  if (!verifier.verify(params.PUBLIC_KEY, sig, 'base64')) {
    return { statusCode: 200, body: { op: 'exception', message: 'Invalid signature' } }
  }

  const payload = JSON.parse(Buffer.from(params.__ow_body, 'base64').toString())
  const { sku, quantity } = payload.product || {}

  // Placeholder rule: replace with your own stock or eligibility check
  if (sku && sku.startsWith('RESTRICTED-') && quantity > 1) {
    return { statusCode: 200, body: { op: 'exception', message: 'Only one of this item per order.' } }
  }
  return { statusCode: 200, body: { op: 'success' } }
}

exports.main = main

Changing data with replace

An after-type hook can tweak the result. This mirrors Adobe’s shipping example, where a nested amount is replaced.

[
  { "op": "replace", "path": "result/shipping_methods/shipping_method_one/amount", "value": 6 }
]

An event handler action

Events don’t expect operations back. Your job is to process the data safely and once. Here’s a simple handler with a duplicate check. The store helper is a placeholder for whatever state store you use.

async function main (params) {
  const eventId = params.event_id || params.id
  const data = params.data?.value || {}

  if (await store.seen(eventId)) {
    return { statusCode: 200, body: { skipped: eventId } }
  }
  await pushToErp({ sku: data.sku, id: data.entity_id }) // placeholder
  await store.mark(eventId)
  return { statusCode: 200, body: { ok: true } }
}
exports.main = main

Decision table: webhook or event?

Use casePickWhy
Block add-to-cart for restricted itemsWebhookYou must stop the flow before it completes.
Add or adjust shipping methodsWebhookOnly a sync response can change the result.
Validate an address or postcode at checkoutWebhookThe shopper needs the answer now.
Send new orders to an ERPEventNo need to block checkout, and retries help.
Sync product changes to a PIM or search toolEventHigh volume, tolerant of seconds of delay.
Notify a CRM about new customersEventFire and forget, with journal replay.
Alert when stock drops below a thresholdEvent with rulesRules filter the noise for you.

My rule of thumb is simple. If a slow or broken endpoint should break the storefront, use a webhook. Otherwise, use an event.

Gotchas I’d plan for

  • Latency: every webhook adds a network round trip to a live request. Keep soft timeouts tight and responses tiny.
  • Idempotency: event retries mean the same event can arrive more than once. Dedupe on an ID.
  • Ordering: I wouldn’t assume events arrive in order. Compare timestamps or re-read the record before you overwrite newer data.
  • Retries: webhooks don’t retry for you. A failed required hook simply fails the request.
  • Payload size: map only the fields you need, on both sides. Big payloads cost time and money.
  • Security: sign webhooks, keep keys in .env, and never send full payloads with personal data you don’t need.

Troubleshooting

The webhook never fires

Check that the subscription is active and the method is in the Webhooks List. In ACCS, use Select > Show Logs on the subscription row.

Checkout hangs or fails randomly

A required hook is probably timing out. Set a real hard timeout, then look at soft-timeout notices in the logs.

Events arrive late

Non-priority events can take up to 59 seconds. Mark the subscription as priority if that delay matters.

Registration shows as disabled

Your endpoint kept failing. Fix it, edit the registration in the Developer Console, and pull missed events from the Journal.

Checklist

  • ☐ Each old plugin and observer mapped to a webhook, an event or a redesign.
  • ☐ Event or webhook method confirmed in the ACCS lists.
  • ☐ Hook fields and event fields trimmed to the minimum.
  • ☐ Hard and soft timeouts set on every hook.
  • ☐ Required flag and fallback message chosen on purpose.
  • ☐ Signature verification enabled and tested.
  • ☐ Event handlers safe against duplicates.
  • ☐ Journal replay tested once before go-live.

FAQ

Can a webhook and an event use the same trigger?

Yes. You might validate with a webhook and then sync with an event from the same flow. Just keep their jobs separate.

Do webhooks retry like events?

Not in the same way. The docs describe timeouts and required or optional behaviour, not an I/O-style retry window.

Is there a replacement for around plugins?

Not a direct one. Most around plugins split cleanly into a before webhook and an after webhook.

What if my event or webhook isn’t in the ACCS list?

Adobe says to open a support ticket. Meanwhile, look for a nearby method that carries the data you need.

Do I need App Builder for webhooks?

No, any HTTPS endpoint works. App Builder is just the easiest place to host one. For product data flows, also see Catalog Service and Live Search in ACCS.

My take

I like this split more than I expected. Plugins and observers made it far too easy to slow down checkout without noticing. Webhooks force you to think about timeouts, and events force you to think about retries. Both are good habits.

Still, I’d keep webhooks rare. Each one sits on a live request, and the storefront is only as fast as your slowest hook. If you’re planning a move, start with my PaaS to ACCS migration checklist and the ACCS vs Adobe Commerce on Cloud comparison. And if a step here doesn’t match your instance, let me know in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *