{"templateId":"markdown","versions":[{"version":"2.0","label":"v2.0","link":"/percents-api/auth-security/webhooks","default":true,"active":true,"folderId":"27d36c3a"}],"sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Webhooks","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhooks","__idx":0},"children":["Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Percents sends webhooks when qualifying reward events occur. Webhooks are delivered with a stable envelope and a signature header."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"envelope","__idx":1},"children":["Envelope"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"webhookId\": \"evt_77777777-7777-4777-8777-777777777777\",\n  \"type\": \"qualified_settlement\",\n  \"data\": {\n    \"eventId\": \"evt_88888888-8888-4888-8888-888888888888\",\n    \"eventType\": \"qualified_settlement\",\n    \"occurredAt\": \"2026-07-01T18:45:20.000Z\",\n    \"chgId\": \"chg_22222222-2222-4222-8222-222222222222\",\n    \"merchantId\": \"mp_33333333-3333-4333-8333-333333333333\",\n    \"currency\": \"usd\",\n    \"sourceTransactionId\": \"txn_55555555-5555-4555-8555-555555555555\",\n    \"rewardEffects\": [\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 1250,\n        \"reasonCode\": \"earn\",\n        \"timing\": \"posted\"\n      }\n    ]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Process each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhookId"]}," once and make replay handling idempotent."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"webhook-signatures","__idx":2},"children":["Webhook Signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Percents signs every webhook so issuers can verify that the request originated from Percents before processing the body."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Each issuer receives a webhook signing token. Signing tokens use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sign_"]}," prefix, are only viewable once, and should be stored securely by the issuer. If a signing token is exposed, request token rotation from Percents and store the replacement token when it is shown."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The signature is sent in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-PERCENTS-SIGNATURE"]}," header."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"t=<epoch_milliseconds>,s=<hex_hmac_sha256_signature>\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Example:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"t=1723493048949,s=9c1a1255a28407f25b6add5d3ec273b16da573853768aa7499ee1d53acb92e35\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The timestamp is Unix epoch time in milliseconds."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To verify a webhook signature:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Read the raw request body string exactly as received, before JSON parsing."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Split the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-PERCENTS-SIGNATURE"]}," header into the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," timestamp and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["s"]}," signature values."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Build the signed value as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<timestamp>.<raw request body>"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compute an HMAC-SHA256 hex digest using the issuer webhook signing token."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compare the computed digest with the signature from the header using a constant-time comparison."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Only process the webhook after the signature is valid."]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"import { createHmac, timingSafeEqual } from 'node:crypto';\n\nfunction verifyPercentsWebhookSignature({\n  header,\n  rawBody,\n  signingToken,\n}: {\n  header: string | undefined;\n  rawBody: string;\n  signingToken: string;\n}) {\n  if (!header) {\n    return false;\n  }\n\n  const [, timestamp, signature] = header.match(/^t=(\\d+),s=([a-f0-9]+)$/i) || [];\n  if (!timestamp || !signature) {\n    return false;\n  }\n\n  const expectedSignature = createHmac('sha256', signingToken)\n    .update(`${timestamp}.${rawBody}`)\n    .digest('hex');\n\n  const expected = Buffer.from(expectedSignature, 'hex');\n  const received = Buffer.from(signature, 'hex');\n\n  return expected.length === received.length && timingSafeEqual(expected, received);\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"event-types","__idx":3},"children":["Event Types"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Type"},"children":["Type"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Meaning"},"children":["Meaning"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["qualified_auth"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A card authorization event qualified. Effects are usually ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["preview"]}," timing."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["qualified_settlement"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A settled transaction qualified and posted. Effects are usually ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["posted"]}," timing."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["clawback"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A previously posted effect was reversed. Effects use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["reversed"]}," timing and clawback reason codes."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"example-webhooks","__idx":4},"children":["Example Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["These examples show the public envelope that is delivered to issuer webhook endpoints. Amounts are positive minor-unit values; the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["reasonCode"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["timing"]}," fields describe how the value should be interpreted."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"qualified_auth","__idx":5},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["qualified_auth"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Sent when a card authorization event qualifies for a reward. Preview effects can be used to inform the issuer experience, but they are not settled liability."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"webhookId\": \"evt_11111111-1111-4111-8111-111111111111\",\n  \"type\": \"qualified_auth\",\n  \"data\": {\n    \"eventId\": \"evt_21111111-1111-4111-8111-111111111111\",\n    \"eventType\": \"qualified_auth\",\n    \"occurredAt\": \"2026-07-01T18:42:11.000Z\",\n    \"chgId\": \"chg_22222222-2222-4222-8222-222222222222\",\n    \"merchantId\": \"mp_33333333-3333-4333-8333-333333333333\",\n    \"currency\": \"usd\",\n    \"sourceAuthId\": \"auth_55555555-5555-4555-8555-555555555555\",\n    \"rewardEffects\": [\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 250,\n        \"reasonCode\": \"earn\",\n        \"timing\": \"preview\"\n      }\n    ]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"qualified_settlement","__idx":6},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["qualified_settlement"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Sent when a settled transaction posts reward effects. A single settlement can include both branded balance spend and newly earned value."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"webhookId\": \"evt_12222222-2222-4222-8222-222222222222\",\n  \"type\": \"qualified_settlement\",\n  \"data\": {\n    \"eventId\": \"evt_22222222-2222-4222-8222-222222222222\",\n    \"eventType\": \"qualified_settlement\",\n    \"occurredAt\": \"2026-07-01T18:45:20.000Z\",\n    \"chgId\": \"chg_22222222-2222-4222-8222-222222222222\",\n    \"merchantId\": \"mp_33333333-3333-4333-8333-333333333333\",\n    \"currency\": \"usd\",\n    \"sourceAuthId\": \"auth_55555555-5555-4555-8555-555555555555\",\n    \"sourceTransactionId\": \"txn_66666666-6666-4666-8666-666666666666\",\n    \"rewardEffects\": [\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 400,\n        \"reasonCode\": \"spend\",\n        \"timing\": \"posted\"\n      },\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 130,\n        \"reasonCode\": \"earn\",\n        \"timing\": \"posted\"\n      }\n    ]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For branded balance, posted spend effects should be represented as an statement credit on the cardholder account in the issuer platform."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"clawback","__idx":7},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["clawback"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Sent when a previously posted reward effect is reversed. Work with the Percents account manager to validate refund flows."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"webhookId\": \"evt_13333333-3333-4333-8333-333333333333\",\n  \"type\": \"clawback\",\n  \"data\": {\n    \"eventId\": \"evt_23333333-3333-4333-8333-333333333333\",\n    \"eventType\": \"clawback\",\n    \"occurredAt\": \"2026-07-02T10:15:00.000Z\",\n    \"chgId\": \"chg_22222222-2222-4222-8222-222222222222\",\n    \"merchantId\": \"mp_33333333-3333-4333-8333-333333333333\",\n    \"currency\": \"usd\",\n    \"sourceTransactionId\": \"txn_77777777-7777-4777-8777-777777777777\",\n    \"rewardEffects\": [\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 130,\n        \"reasonCode\": \"earn_clawback\",\n        \"timing\": \"reversed\"\n      },\n      {\n        \"appliedOfferId\": \"mpo_44444444-4444-4444-8444-444444444444\",\n        \"amountMinor\": 400,\n        \"reasonCode\": \"spend_clawback\",\n        \"timing\": \"reversed\"\n      }\n    ]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"reward-effects","__idx":8},"children":["Reward Effects"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Field"},"children":["Field"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Meaning"},"children":["Meaning"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["appliedOfferId"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The offer that produced the effect when available."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["amountMinor"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Effect amount in minor units."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["reasonCode"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["earn"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["spend"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["earn_clawback"]},", or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["spend_clawback"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["timing"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["preview"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["posted"]},", or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["reversed"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Preview effects should inform the issuer experience but should not be treated as settled liability. Posted effects are final. Reversed effects undo prior posted value."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"suppression","__idx":9},"children":["Suppression"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Percents may suppress a webhook when processing determines that no partner-visible event should be delivered. A suppressed webhook is not delivered."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Common suppression cases include no qualifying settlement, a duplicate or idempotent replay with no new posted effect, or a configured workflow that should not notify the issuer for a particular event."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"retries-and-backoff","__idx":10},"children":["Retries And Backoff"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Percents attempts delivery immediately. If the endpoint does not return a successful HTTP response, the event is retried up to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["5"]}," times by default."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Retry schedule:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Retry attempt"},"children":["Retry attempt"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Next attempt after"},"children":["Next attempt after"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["0"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["1 hour"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["1"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["2 hours"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["2"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["4 hours"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["3"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["12 hours"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["4"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["24 hours"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["5"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["72 hours"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["After retries are exhausted, delivery is not retried automatically. Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhookId"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["type"]},", and the ids in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["data"]}," to reconcile with activity and transaction detail endpoints."]}]},"headings":[{"value":"Webhooks","id":"webhooks","depth":1},{"value":"Envelope","id":"envelope","depth":2},{"value":"Webhook Signatures","id":"webhook-signatures","depth":2},{"value":"Event Types","id":"event-types","depth":2},{"value":"Example Webhooks","id":"example-webhooks","depth":2},{"value":"qualified_auth","id":"qualified_auth","depth":3},{"value":"qualified_settlement","id":"qualified_settlement","depth":3},{"value":"clawback","id":"clawback","depth":3},{"value":"Reward Effects","id":"reward-effects","depth":2},{"value":"Suppression","id":"suppression","depth":2},{"value":"Retries And Backoff","id":"retries-and-backoff","depth":2}],"frontmatter":{"seo":{"title":"Webhooks"}},"lastModified":"2026-07-21T18:22:26.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/percents-api/auth-security/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}