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.
Benefits of using TapTree webhooks:
- Real-Time Data Processing: Instantly receive notifications about specific activities in TapTree.
- Automated Workflows: Trigger actions in your application, such as updating records or initiating processes.
- Efficient Resource Usage: Avoid constant polling of API endpoints for updates, reducing server load.
Setting Up Webhooks
TapTree offers two ways to receive webhook events. You can use either, or both:
| Mechanism | Where you configure it | When events fire |
|---|---|---|
Per-payment webhook_url | At payment creation, via the webhook_url field on the payment object | Only for events tied to that one payment (lifecycle + its refunds + chargebacks) |
| Static endpoints | In 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:
-
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).
-
Register the endpoint with TapTree by providing a
webhook_urlto TapTree API endpoints supporting webhooks. For example, when creating a new payment object, you can provide awebhook_urlto 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 samewebhook_url. -
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
idto idempotently process webhook events. For instance, if you receive a webhook event with the sameidtwice, 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-DDindicating when this specific event object was changed by us.
- Name
mode- Type
- string
- Description
The
modeattribute of each event specifies whether it's from yourtestorliveenvironment. 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.
Retrieve your webhook secret in the TapTree Dashboard under Entwickler → API-Schlüssel (the merchant dashboard is German).
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 likewh_BDgCjn9hJHR; for static endpoints likewhsec_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: AlwaysHMAC.content-type: Alwaysapplication/json.
signature-algo distinguishes the versions:
signature-algo value | Endpoint type | Canonical input that was signed | Extra header |
|---|---|---|---|
sha256 | Per-payment webhook_url (v1) | the JSON-encoded request body as received over the wire | — |
hmac-sha256-v2 | Static 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).hexmust equalsignature.hmac-sha256-v2(v2, static endpoints):HMAC-SHA256(secret, "${signature-timestamp}.${raw_body}").hexmust equalsignature. Additionally check thatsignature-timestampis 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')
},
)
If you must use a framework that consumes the body before your handler runs (e.g., a JSON middleware that's hard to disable), look for the framework's raw-body hook: Express has bodyParser.json({ verify: (req, _, buf) => { req.rawBody = buf } }), Fastify has addContentTypeParser('application/json', { parseAs: 'buffer' }, ...), Django exposes request.body. The signature must always be computed against those exact bytes.
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 Number | Time After Previous Attempt | Cumulative Time from Start |
|---|---|---|
| 1 | 1 min | 1 min |
| 2 | 2 min | 3 min |
| 3 | 4 min | 7 min |
| 4 | 8 min | 15 min |
| 5 | 15 min | 30 min |
| 6 | 30 min | 60 min (1 hr) |
| 7 | 60 min (1 hr) | 120 min (2 hrs) |
| 8 | 720 min (12 hrs) | 840 min (14 hrs) |
| 9 | 1920 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
idto prevent processing the same event more than once. One way to do this is to store theidof each event you process in your database, and ignore events withidsyou've already seen. -
Only listen to events you care about: When creating resource objects, you can use the
webhook_urlto 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
200status 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
idof each event you process in your database, and ignore events withidsyou've already seen. Moreover, each event has atriggered_attimestamp, which you can use to ignore events that are too old. Note thattriggered_attimestamp 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 sametriggered_attimestamp as the original event.