Use Webhooks for Real-time Updates

Webhooks in the TapTree API provide a method for your application to receive real-time notifications about events, such as payments, refunds, and chargebacks. This documentation will guide you through handling and verifying webhook events.

Understanding Webhooks

When building an application that integrates with TapTree, you may want to know when certain events happen in your TapTree accounts to take corresponding actions in your backend. For instance, you might want to know when a payment is paid so that you can fulfill an order, or when a customer disputes a payment so that you can investigate the dispute. TapTree uses HTTPS to send these webhook events as JSON data to your application. Each message contains an Event object.

Setting Up Webhooks

TapTree offers two ways to receive webhook events. You can use either, or both:

MechanismWhere you configure itWhen events fire
Per-payment webhook_urlAt payment creation, via the webhook_url field on the payment objectOnly for events tied to that one payment (lifecycle + its refunds + chargebacks)
Static endpointsIn the TapTree Dashboard under Entwickler → Webhooks (the merchant dashboard is German)For every event of the types you subscribe to, across all payments in the same environment

In both cases your server endpoint must accept HTTPS POST with application/json bodies and verify the HMAC signature before acting on the payload. The signing scheme differs slightly between the two — see Verifying Signatures for the per-mechanism details.

  • Static endpoints: Dashboard-managed URLs scoped per environment, with wildcard event subscriptions, secret rotation with a 24h grace, tamper-evident audit log, and auto-disable on sustained failures. Learn more →

Per-payment webhooks

A fine-grained three-step process:

  1. Retrieve your webhook secret, which we use to sign the data of your events. You can retrieve a webhook secret in the TapTree Dashboard under Entwickler → API-Schlüssel (German dashboard).

  2. Register the endpoint with TapTree by providing a webhook_url to TapTree API endpoints supporting webhooks. For example, when creating a new payment object, you can provide a webhook_url to be notified by us about events on that payment's lifecycle (authorized, paid, canceled, failed, expired, etc.). Refund and chargeback events related to this payment are automatically sent to the same webhook_url.

  3. Handle the incoming webhook events by reading the raw JSON body and verifying the signature. If the signature is valid, you can be sure that the webhook event was sent by us and not by a third party. You can then process the event and take the appropriate action in your application.

Handling Event Objects

When you've registered a webhook_url for a payment, we will send you a webhook event whenever something notable happens to that payment, its refunds, or its chargebacks. In particular, we create a specific Event object to keep you informed and ready to act. This object contains the following attributes.

  • Name
    id
    Type
    string
    Description

    A unique identifier for the webhook event. We recommend to use this id to idempotently process webhook events. For instance, if you receive a webhook event with the same id twice, you should only process it once based on the id.

  • Name
    type
    Type
    string
    Description

    The type of event that occurred. For instance, transactions.payment.paid.

  • Name
    triggered_at
    Type
    string
    Description

    The time at which the event was triggered in our systems.

  • Name
    data
    Type
    object
    Description

    The data associated with the event. This will vary depending on the event type.

  • Name
    version
    Type
    string
    Description

    The version of the event. This will be a date in the format YYYY-MM-DD indicating when this specific event object was changed by us.

  • Name
    mode
    Type
    string
    Description

    The mode attribute of each event specifies whether it's from your test or live environment. This distinction is particularly helpful if you're using the same endpoint for handling events in both environments, allowing you to easily differentiate and appropriately process these events based on their origin.

Example event object

{
  "id": "evt_a056V7R7NmNRjl70",
  "object": "event",
  "type": "transactions.payment.paid",
  "triggered_at": "2023-12-25T01:16:28.297Z",
  "version": "2026-05-16",
  "mode": "test",
  "data": {
    "object": "payment",
    "id": "pay_2VUVYcuruVv",
    "org_id": "org_5A8hsNqGGyW",
    "acceptor_id": "acceptor_Aa54Z3THuFj",
    "mode": "test",
    "status": "paid",
    "created_at": "2023-12-25T01:16:15.352Z",
    "paid_at": "2023-12-25T01:16:28.279Z",
    "description": "order #2",
    "amount": {
      "value": "15.00",
      "currency": "eur"
    },
    "impact": {
      "value": "1081",
      "unit": "g CO2"
    },
    "payment_method": "card",
    "card": {
      "bin": "411111",
      "brand": "visa",
      "last4": "1111",
      "wallet": "googlepay",
      "security": "basic",
      "expiry_year": "2025",
      "expiry_month": "12"
    }
  }
}

Event Types

The type field in the Event object indicates what happened. The catalog spans payments, refunds, chargebacks, settlements, and organization-lifecycle events.

See the full, always-current list of event types in your Dashboard at Entwickler → Webhooks → Endpoint anlegen → Event-Types — the picker is fed by the live catalog so subscriptions stay in sync with what we emit.

Chargeback events require immediate attention. When you receive a transactions.chargeback.open event, you must accept or dispute the chargeback before the deadline_at date in the chargeback object. If no action is taken, the chargeback is automatically accepted and the funds are returned to the cardholder. See Chargebacks for details on the dispute process.

Example Event Objects

Below are example payloads for different event types. Note that the data field varies depending on the type of event.

Example event objects

{
  "id": "evt_a056V7R7NmNRjl70",
  "object": "event",
  "type": "transactions.payment.paid",
  "triggered_at": "2023-12-25T01:16:28.297Z",
  "version": "2026-05-16",
  "mode": "test",
  "data": {
    "object": "payment",
    "id": "pay_2VUVYcuruVv",
    "org_id": "org_5A8hsNqGGyW",
    "acceptor_id": "acceptor_Aa54Z3THuFj",
    "mode": "test",
    "status": "paid",
    "created_at": "2023-12-25T01:16:15.352Z",
    "paid_at": "2023-12-25T01:16:28.279Z",
    "description": "order #2",
    "amount": {
      "value": "15.00",
      "currency": "eur"
    }
  }
}

Securing Webhooks

Once you're set up to receive webhook events, the next crucial step is securing your endpoint. This ensures the events you process are genuinely from TapTree, not imposters.

Each webhook event comes with a signature header, created using HMAC SHA256 and your unique webhook secret. Verifying this signature is your safeguard. Simply compute the HMAC SHA256 signature of the webhook's body with your secret key and match it against the signature we sent. A match confirms the event's authenticity, while a mismatch is a red flag to discard the event.

Be wary of potential threats: without signature verification, malicious entities could mimic TapTree, triggering false events. This could lead to unwarranted actions like unfulfilled orders or content access, damaging customer trust and incurring financial losses.

Webhook Headers

Every webhook request carries a signature plus identifying metadata. The exact set of headers depends on which signing algorithm version your endpoint received the event under.

Common to both versions:

  • signature-secret-id: Identifies the signing secret used. For per-payment endpoints this looks like wh_BDgCjn9hJHR; for static endpoints like whsec_id_jxwewxff. Use it to pick the right secret if you keep more than one.
  • signature: The HMAC-SHA256 hex digest, e.g., 2081f84b30fb742bf9552121545e49f6ee138e2f2b5a09be1c6d992b8dc799e6.
  • signature-method: Always HMAC.
  • content-type: Always application/json.

signature-algo distinguishes the versions:

signature-algo valueEndpoint typeCanonical input that was signedExtra header
sha256Per-payment webhook_url (v1)the JSON-encoded request body as received over the wire
hmac-sha256-v2Static endpoints (v2)${signature-timestamp}.${raw_body}signature-timestamp (epoch seconds, e.g., 1779024927)

The signature-timestamp header doubles as replay-attack defense: reject events whose timestamp is more than a few minutes old (e.g., 5 min) in addition to the signature check.

For everything else the v2 algorithm implies — per-environment scoping, wildcard subscriptions, secret rotation with 24h overlap, audit log, auto-disable — see Static Endpoints.

Verifying Signatures

Always HMAC over the raw request body bytes — never a re-serialized JSON object. Different JSON libraries disagree on key order, whitespace, number formatting, and unicode escaping. Re-stringifying a parsed payload produces bytes that look identical but differ in invisible ways, which silently breaks signature verification. Capture the raw body before any JSON parser touches it.

The verification recipe is one of two shapes, picked by the signature-algo header:

  • sha256 (v1, per-payment endpoints): HMAC-SHA256(secret, raw_body).hex must equal signature.
  • hmac-sha256-v2 (v2, static endpoints): HMAC-SHA256(secret, "${signature-timestamp}.${raw_body}").hex must equal signature. Additionally check that signature-timestamp is within an acceptable freshness window (e.g., 5 minutes) to defend against replay.

The examples below handle both shapes in one handler. Always compare digests in constant time (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) — a plain == leaks signature bytes via timing.

For runnable Node, Python, PHP and Ruby snippets that also handle rotation by signature-secret-id, plus a going-live runbook, see Webhook Testing. For the envelope-encryption design behind one-shot reveal and rotation, see Webhook Signing Secrets.

Verifying the Signature

import express from 'express'
import crypto from 'crypto'

const app = express()
const SECRET = process.env.WEBHOOK_SECRET
const MAX_AGE_SECONDS = 5 * 60

// CRITICAL: capture the raw body BEFORE JSON parsing.
// express.raw() leaves req.body as a Buffer of the exact bytes we signed.
app.post(
  '/webhooks/taptree',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['signature']
    const algo = (req.headers['signature-algo'] || '').toLowerCase()
    const timestamp = req.headers['signature-timestamp']

    let canonical // Buffer or string — what we HMAC
    if (algo === 'hmac-sha256-v2') {
      if (!timestamp) return res.status(400).send('missing signature-timestamp')
      const ageSec = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)
      if (!Number.isFinite(ageSec) || ageSec > MAX_AGE_SECONDS) {
        return res.status(401).send('stale timestamp')
      }
      canonical = `${timestamp}.${req.body.toString('utf8')}`
    } else if (algo === 'sha256') {
      canonical = req.body // raw Buffer
    } else {
      return res.status(400).send('unsupported signature-algo')
    }

    const expected = crypto.createHmac('sha256', SECRET).update(canonical).digest('hex')

    const ok =
      signature?.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(signature, 'hex'),
        Buffer.from(expected, 'hex'),
      )
    if (!ok) return res.status(401).send('invalid signature')

    const event = JSON.parse(req.body.toString('utf8'))
    // ... process event idempotently using event.id ...
    res.status(200).send('ok')
  },
)

Grasping Delivery Behavior

The delivery behavior of webhooks is an important aspect to consider when building your integration. In this section, we'll look at the most important aspects such as retries and delivery order.

Retry Mechanism

We retry sending webhooks if we don't receive a 2xx response within 30 seconds from you. We retry for up to 46 hours with an exponential backoff. Test and live endpoints use the same retry schedule.

Here's a breakdown of the retry strategy, showing the pattern of increasing intervals:

Retry NumberTime After Previous AttemptCumulative Time from Start
11 min1 min
22 min3 min
34 min7 min
48 min15 min
515 min30 min
630 min60 min (1 hr)
760 min (1 hr)120 min (2 hrs)
8720 min (12 hrs)840 min (14 hrs)
91920 min (32 hrs)2760 min (46 hrs)

Delivery Order

We attempt to deliver events in the order they occur, but ordering is not guaranteed. For example, if a new event fires while we're retrying a previous one, the new event may arrive first.

Best Practices

There are a few best practices to keep in mind when working with webhooks:

  • Handle duplicate events: Utilize the event id to prevent processing the same event more than once. One way to do this is to store the id of each event you process in your database, and ignore events with ids you've already seen.

  • Only listen to events you care about: When creating resource objects, you can use the webhook_url to specify if you want to be notified about events happening to that object. This allows you to only listen to events you care about, and ignore the rest.

  • Quickly respond with 2xx: Always return a 2xx status code quickly, before any extensive processing. Making sure that timeouts do not occur and your customers are not waiting for your response is crucial. If you need to do some heavy processing, store the webhook event in your database, respond with a 200 status and process the event asynchronously.

  • Verify events are sent from TapTree: You should always verify our signature. We sign every webhook event with a secret that is unique to your account. You can retrieve your webhook secret in the TapTree Dashboard under Entwickler → API-Schlüssel (the merchant dashboard is German).

  • Roll signing secrets regularly: You can roll your webhook signing secret at any time, and you should do so immediately if you suspect it has been compromised. For per-payment endpoints (v1), rolling invalidates the previous secret immediately. For static endpoints (v2), the previous secret remains valid for a 24-hour grace period to avoid mid-rotation drops; verify against the newest secret first and fall back to the previous one only if it fails.

  • Prevent replay attacks: A replay attack is when an attacker intercepts a webhook event and sends it again later. To prevent this, you should store the id of each event you process in your database, and ignore events with ids you've already seen. Moreover, each event has a triggered_at timestamp, which you can use to ignore events that are too old. Note that triggered_at timestamp is not the time at which the event was sent to you, but the time at which the event was triggered in our systems. So all retries will have the same triggered_at timestamp as the original event.

Was this page helpful?