Wompi: how to generate the integrity signature and verify webhooks, with code

How Wompi's checkout integrity signature is computed and how to check that an event really comes from Wompi, with Python and JavaScript functions tested against the example in its documentation.

In this article
In 30 seconds

With Wompi you sign the payment on your server with a SHA-256 of the reference, amount in cents, currency and your integrity secret, and you verify each event by recomputing a SHA-256 from the properties the event declares, its timestamp and your events secret.

1. Two different signatures, two different secrets#

Integrating Wompi on a site involves two checks that are easy to confuse. The integrity signature protects the payment your server hands to the checkout: it prevents someone from changing the amount in the browser. Event verification protects your server: it checks that the notification of a status change comes from Wompi and not from a third party.

Each uses a different secret, which is neither the public nor the private key. According to Wompi's documentation, both are in the merchant dashboard, in the technical integration secrets section, and must not leave your server.

Documentation: Wompi: Widget & Web Checkout (integrity signature, Spanish) ↗ · Wompi: Events (checksum verification, Spanish) ↗

2. The checkout integrity signature#

The documentation says the signature is the SHA-256, in hexadecimal, of the concatenation of the reference, the amount in cents, the currency and the integrity secret. If the payment has an expiration date, that date goes between the currency and the secret. The amount goes in cents: 95,000 pesos are sent as 9500000, and the only currency available is COP.

ItemValueNote
Referencesk8-438k4-xmxm392-sn2mYour unique payment identifier
Amount2490000$24,900 COP in cents
CurrencyCOPThe only one available today
Secretprod_integrity_…Never in the browser
Stringsk8-438k4-xmxm392-sn2m2490000COPprod_integrity_…SHA-256 is applied, in hexadecimal

The table rebuilds the documentation's example: a reference, an amount of 24,900 pesos (2490000 cents) and the example secret. The code below produces exactly the hash the documentation publishes for that string.

3. Verifying an event (webhook)#

To check an event, the documentation says to recompute the checksum like this: read the signature.properties list from the event itself; look up each path inside the data field and concatenate the values in that order; append the event's timestamp and, last, the events secret; apply SHA-256 and compare with the received checksum, case-insensitively. If they do not match, ignore the event.

The list of properties can vary, so do not hardcode it: read it from the event, as the functions below do. The functions compare in constant time.

python
import hashlib
import hmac

def integrity_signature(reference, amount_in_cents, currency, secret, expiration=""):
    """Checkout signature: SHA-256 of reference + amount + currency [+ expiration] + secret."""
    text = f"{reference}{amount_in_cents}{currency}{expiration}{secret}"
    return hashlib.sha256(text.encode()).hexdigest()

def value_at(data, path):
    """Reads a path such as 'transaction.status' inside the event's data field."""
    current = data
    for part in path.split("."):
        current = current[part]
    return str(current)

def is_authentic_event(event, events_secret):
    """Recomputes the checksum with the properties the event itself declares."""
    sig = event["signature"]
    values = "".join(value_at(event["data"], p) for p in sig["properties"])
    text = f"{values}{event['timestamp']}{events_secret}"
    expected = hashlib.sha256(text.encode()).hexdigest()
    return hmac.compare_digest(expected.lower(), sig["checksum"].lower())

# Example from Wompi's documentation: $24,900 COP is 2490000 cents
print(integrity_signature("sk8-438k4-xmxm392-sn2m", 2490000, "COP", "prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqsI6SFli6"))

# Test event with made-up values and secret
secret = "test_events_example_secret"
event = {
    "data": {"transaction": {"id": "T-0001", "status": "APPROVED", "amount_in_cents": 9500000}},
    "timestamp": 1760000000,
    "signature": {"properties": ["transaction.id", "transaction.status", "transaction.amount_in_cents"]},
}
text = "T-0001" + "APPROVED" + "9500000" + "1760000000" + secret
event["signature"]["checksum"] = hashlib.sha256(text.encode()).hexdigest().upper()
print(is_authentic_event(event, secret), is_authentic_event(event, "another_secret"))
Python 3, standard library only. The first line is the hash of the documentation's example; the second, a valid test event and the same event with another secret.
javascript
import { createHash, timingSafeEqual } from "node:crypto";

const sha256 = text => createHash("sha256").update(text).digest("hex");

function integritySignature(reference, amountInCents, currency, secret, expiration = "") {
  return sha256(`${reference}${amountInCents}${currency}${expiration}${secret}`);
}

function valueAt(data, path) {
  return String(path.split(".").reduce((current, part) => current[part], data));
}

function isAuthenticEvent(event, eventsSecret) {
  const { properties, checksum } = event.signature;
  const values = properties.map(p => valueAt(event.data, p)).join("");
  const expected = Buffer.from(sha256(`${values}${event.timestamp}${eventsSecret}`));
  const received = Buffer.from(checksum.toLowerCase());
  return expected.length === received.length && timingSafeEqual(expected, received);
}

console.log(integritySignature("sk8-438k4-xmxm392-sn2m", 2490000, "COP", "prod_integrity_Z5mMke9x0k8gpErbDqwrJXMqsI6SFli6"));

const secret = "test_events_example_secret";
const event = {
  data: { transaction: { id: "T-0001", status: "APPROVED", amount_in_cents: 9500000 } },
  timestamp: 1760000000,
  signature: { properties: ["transaction.id", "transaction.status", "transaction.amount_in_cents"] },
};
event.signature.checksum = sha256("T-0001" + "APPROVED" + "9500000" + "1760000000" + secret).toUpperCase();
console.log(isAuthenticEvent(event, secret), isAuthenticEvent(event, "another_secret"));
JavaScript (Node.js, ES module). Prints the same hash and true false.

One important detail: when we recomputed the documentation's events example with the values it lists, we did not obtain the checksum it publishes. That is why this guide's test event uses made-up values and secret, and we recommend validating your implementation with real events from the sandbox environment before going to production.

Documentation: Python: hashlib ↗ · Node.js: crypto ↗

4. What to do after verifying#

  • Answer the webhook quickly and do the heavy work afterwards, so you do not exhaust the sender's timeout.
  • Treat events as idempotent: store the transaction id and do not apply the same status twice.
  • Compare the event's amount and reference with your order, do not trust the status alone.
  • Consider rejecting events with a very old timestamp; this is an extra measure of yours, not a documentation requirement.
  • Log discarded events without storing secrets.

Go deeperQueues, Retries, and Idempotency: Stop Processing Messages Twice

5. Common mistakes#

  • Computing the integrity signature in the browser and exposing the secret.
  • Sending the amount in pesos instead of cents.
  • Mixing up the integrity secret with the events secret.
  • Hardcoding the event's property list instead of reading it from the event.
  • Comparing the checksum with == instead of a constant-time comparison.

Go deeperEnvironment variables and DATABASE_URL in production: a full guide

6. Keep your numbers current#

The integration is described as Wompi's documentation stood on October 18, 2026. The fields, the available currencies or the algorithm may change; check the official documentation and test in sandbox before deploying.

  • Check Wompi's documentation before every integration change.
  • Test with approved, declined and voided sandbox transactions.
  • Rotate the secrets if you suspect they leaked.

Sources and scope

Documentation checked on October 18, 2026. Examples and decision criteria are editorial proposals; adapt them to your application's contract and validate them in an authorized test environment.

Compare these options

Compare databases

Review pricing, limits, conditions and sources for each option.

Open comparison
Advice

Is your case different?

Tell us your context and the options you're weighing. We reply with criteria and sources, and never sign you up for anything.

Request advice