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.
| Item | Value | Note |
|---|---|---|
| Reference | sk8-438k4-xmxm392-sn2m | Your unique payment identifier |
| Amount | 2490000 | $24,900 COP in cents |
| Currency | COP | The only one available today |
| Secret | prod_integrity_… | Never in the browser |
| String | sk8-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.
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"))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"));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.