---
updatedAt: 2026-04-23T15:53:20.000Z
agentTools:
  projectIndex: https://developers.privacy.com/llms.txt
---

# Transaction Webhooks

Learn about transaction webhooks.

Privacy transaction webhooks are used to receive advice messages and are for reporting purposes only. You can not make authorization decisions using these webhooks. Advice messages require only a response code 200 and do not require a response body. If this isn't received (e.g., if the client backend is offline), Privacy will continually attempt to retransmit the message using exponential backoff (doubling the time between messages each iteration) until the backoff period exceeds 24 hours.

If transaction webhooks are enabled, we'll send an HTTP `POST` request to a user-specified url (in the account settings page) whenever a transaction lifecycle event occurs. We recommend responding to transactions webhooks with a 200 at time of receipt. If there is no response or a non-200 response, Privacy will retry sending the webhook. Retries are attempted with exponential backoff, starting with the first retry 5 minutes after the webhook and ending when the next attempt would be over a day.

# Message Types

Transaction Events are generated for the following actions:

|               |                                                                                                       |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| Authorization | The API sends an event for all approvals. Decline events are available with API Issuing accounts      |
| Auth Advice   | Transaction was declined by an upstream switch or a previous authorization's amount is being adjusted |
| Void          | Previous pending authorization is voided                                                              |
| Clearing      | Clearing for an existing, pending authorization                                                       |
| Return        | Credit — value is pushed onto card                                                                    |

# HMAC Verification

To verify that the request is legitimate, you may generate an HMAC of the transaction object and compare it with the one included in the `X-Privacy-HMAC` request header.

See below for example implementations of verifying a webhook request.

```javascript Node
const crypto = require("crypto");
const https = require("https");


function hmacSignature(key, msg) {
    return crypto.createHmac("sha256", key)
                 .update(msg)
                 .digest("base64");
}

function requestIsFromLithic(apiKey, transaction, requestHeaders) {
	const requestHmac = requestHeaders["X-Privacy-HMAC"];
  
  const requestJson = JSON.stringify(transaction, Object.keys(transaction).sort());
  const dataHmac = hmacSignature(apiKey, requestJson);

  return crypto.timingSafeEqual(requestHmac, dataHmac);
}
```
```python
import base64
import hashlib
import hmac
import json


def to_json_str(json_object):
    return json.dumps(json_object, sort_keys=True, separators=(',', ':'))


def hmac_signature(key, msg):
    hmac_buffer = hmac.new(
        key=bytes(key, 'utf-8'),
        msg=bytes(msg, 'utf-8'),
        digestmod=hashlib.sha256
    )
    return base64.b64encode(hmac_buffer.digest()).decode('utf-8')


def request_is_from_privacy(api_key, transaction, request_headers):
    request_hmac = request_headers["X-Privacy-HMAC"]
    data_hmac = hmac_signature(api_key, to_json_str(transaction))

    return hmac.compare_digest(request_hmac, data_hmac)
```
```java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class HmacTester {
    private static final String ALGORITHM = "HmacSHA256";
    private static final ObjectMapper SORTED_MAPPER = new ObjectMapper();

    static {
        // This will sort the properties alphabetically
        SORTED_MAPPER.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
        SORTED_MAPPER.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);  
        // This will remove the indention between keys and values in the requests
        SORTED_MAPPER.disable(SerializationFeature.INDENT_OUTPUT);
    }

    public boolean requestIsFromLithic(String apiKey, Object transaction, String requestHmacHeader) throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeyException {
        String hmacSignature = hmacSignature(apiKey, toJsonStr(transaction));
        return hmacSignature.equals(requestHmacHeader);
    }

    private String hmacSignature(String key, String msg) throws NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), ALGORITHM);
        Mac mac = Mac.getInstance(ALGORITHM);
        mac.init(secretKeySpec);
        byte[] bytes = mac.doFinal(msg.getBytes());
        return new String(Base64.getEncoder().encode(bytes));
    }

    private String toJsonStr(Object transaction) throws JsonProcessingException {
        return SORTED_MAPPER.writeValueAsString(transaction);
    }
}
```
```rust
use base64;
use hmac::{Hmac, Mac};
use serde::Serialize;
use serde_json;
use sha2::Sha256;

fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
    value: &T,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
    value.serialize(serializer)
}

#[derive(Serialize)]
struct SortAlphabetically<T: Serialize>(#[serde(serialize_with = "sort_alphabetically")] T);

#[derive(Serialize)]
struct Transaction {
    token: String,
    amount: i64,
}

fn main() {
    let args: Vec<String> = env::args().collect();

    let api_key = &args[1];
    let hmac_header = &args[2];

    type HmacSha256 = Hmac<Sha256>;

    let mut mac = HmacSha256::new_from_slice(api_key.as_bytes()).unwrap();

    let transaction = Transaction {
        token: "270a4a65-44d0-4fb2-9bf9-59fd860d6b94".to_string(),
        amount: 100,
    };

    let transaction_json = serde_json::to_string(&SortAlphabetically(&transaction)).unwrap();

    mac.update(&transaction_json.as_bytes());

    let code_bytes = base64::decode(hmac_header).unwrap();

    // `verify_slice` will return `Ok(())` if code is correct, `Err(MacError)` otherwise
    mac.verify_slice(&code_bytes[..]).unwrap()
}
```

<Callout icon="📘" theme="info">
  Note the JSON the [HMAC](https://en.wikipedia.org/wiki/HMAC) is generated from has no extra whitespace, uses all double quotes `""` for strings, and sorts the key-value pairs in all JSON objects by key alphabetically.
</Callout>

# Schema

The request payload will contain a [Transaction](https://privacy-com.readme.io/docs/transactions#transaction-schema) object.