1. Transactions Status Notification
PayPaga
  • Introduction
    • Introduction
    • API Reference
    • Environments
    • Payment methods
    • Errors
    • Standard Codes and Values
    • Changelog
  • Authorization
    • Authorization
    • OAuth 2.0 Token Generation
  • Pay In - Direct API Integration
    • Pay In - Direct API Integration
    • Pay In - Payment Options
    • Pay In - Payment Processing
    • Pay In - Query Transactions
  • Pay In - PayURL Integration
    • Pay In - PayURL Integration
    • Pay In - Create PayURL
  • Pay In - Examples
    • Pay In - Argentina
      • Pay In - Instant Transfers
    • Pay In - Brazil
      • Pay In - PIX
    • Pay In - Chile
      • Pay In - Bank Transfer
      • Pay In - Khipu
    • Pay In - Colombia
      • Pay In - Dale
      • Pay In - Daviplata
      • Pay In - Efecty
      • Pay In - Gana
      • Pay In - Movii
      • Pay In - Nequi
      • Pay In - PSE
      • Pay In - RappiPay
      • Pay In - ReFacil
      • Pay In - Susuerte
      • Pay In - Western Union
    • Pay In - Ecuador
      • Pay In - Bank Transfer
      • Pay In - Bemóvil
      • Pay In - Deuna
      • Pay In - Mi Negocio Efectivo
      • Pay In - Omniswitch
      • Pay In - Rapi Activo
      • Pay In - Western Union
    • Pay In - El Salvador
      • Pay In - Banco Agrícola
      • Pay In - Banco Cuscatlán
      • Pay In - Puntoxpress
    • Pay In - Guatemala
      • Pay In - BAM Efectivo
      • Pay In - BAM Transferencia
      • Pay In - Banco Industrial
      • Pay In - Akisi Pronet
    • Pay In - Mexico
      • Pay In - Pay With Cash
      • Pay In - SPEI
    • Pay In - Peru
      • Pay In - BBVA
      • Pay In - BCP
      • Pay In - BCP Efectivo
      • Pay In - Cell Power
      • Pay In - KasNet
      • Pay In - QR Interoperable
      • Pay In - Plin
      • Pay In - Yape
  • Pay Out - Direct API Integration
    • Pay Out - Direct API Integration
    • Pay Out - Query Transactions
    • Pay Out - Payment Processing
  • Pay Out - Examples
    • Pay Out - Argentina
    • Pay Out - Brazil
    • Pay Out - Chile
    • Pay Out - Colombia
    • Pay Out - Ecuador
    • Pay Out - Guatemala
    • Pay Out - Mexico
    • Pay Out - Peru
  • Transactions Status Notification
    • Transactions Status Notification
    • Pay In - Instant Transfers
    • How to verify callback signature
    • Signing public keys
      GET
  • Query Balance
    • Query Balance
  • Appendix
    • Transaction Status Definitions and Lifecycle
    • Assets
  1. Transactions Status Notification

How to verify callback signature

All webhook payloads sent by our system are cryptographically signed using RSA-SHA256. This allows you to verify that each webhook was sent by us and has not been tampered with in transit.

Algorithm#

PropertyValue
AlgorithmRSA-SHA256
Signature EncodingBase64

Headers#

Every webhook request includes the following signature headers:
HeaderDescriptionExample
X-Signature-TimestampUnix timestamp (10 digits, seconds since epoch)1713982800
X-SignatureBase64-encoded RSA signature of the signed payloaddGhpcyBpcyBhbiBleGFtcGxlIHNpZ25hdHVyZSE...
X-Signed-ByPublic key identifier (used to fetch the correct certificate from /certificates)5ac5ae43-01d2-4c70-8a5b-b69a34d11c62

Signed Payload Construction#

To verify the signature, you must reconstruct the exact signed payload that was used during signing:
signed_payload = timestamp + "." + raw_body_bytes
Where:
timestamp is the value from X-Signature-Timestamp header (as a string)
. is a literal period character (ASCII 46)
raw_body_bytes is the raw, unmodified HTTP request body (as bytes, not parsed JSON)

Example#

Given:
X-Signature-Timestamp: 1713982800
Body: {"transaction_id":"20260424-2333-4543-bbc8-482d90a8a960","status":"Error","merchant_id":"2f585060-092c-43af-9efe-2db5439c8448","transaction_type":"Payout","merchant_transaction_reference":"3342798"}
The signed payload is:
1713982800.{"transaction_id":"20260424-2333-4543-bbc8-482d90a8a960","status":"Approved","merchant_id":"2f585060-092c-43af-9efe-2db5439c8448","transaction_type":"Payout","merchant_transaction_reference":"3342798"}
Important: Do not parse, format, or modify the JSON body before verification. Use the raw bytes exactly as received.

Verification Steps#

1.
Extract X-Signature-Timestamp, X-Signature, and X-Signed-By from the request headers
2.
Verify the timestamp is within your tolerance window (recommended: 300 seconds / 5 minutes)
3.
Fetch the public keys from Signing public keys and choose the correct one based on X-Signed-By header value (please refer Public Key Caching section).
4.
Reconstruct the signed payload: timestamp + "." + raw_body
5.
Compute SHA-256 hash of the signed payload
6.
Base64-decode the X-Signature header
7.
Verify the RSA signature using the public key

Public Key Caching#

We recommend caching the certificates response locally to avoid fetching it on every callback notification, we rotate the certificates every month, but we preserve the previous one to ensure all signatures can be verified.
You could use the expires_at field of the latest certificate as TTL.

Implementation Note#

Since only 2 certificates exist at any time, storing the full response is the recommended approach.

Code Examples#

Go#


JavaScript (Node.js)#


C##


Java#


Python#


PHP#


Important Notes#

Always verify before parsing. Do not parse the JSON body until the signature has been verified.
Use raw bytes. Do not convert the body to string, format, or re-serialize the JSON before verification.
Timestamp tolerance. We recommend rejecting webhooks with timestamps older than 5 minutes (300 seconds) to prevent replay attacks.
Key rotation. When we rotate signing keys (every month), a new X-Signed-By identifier will be used.
Previous
Pay In - Instant Transfers
Next
Signing public keys
Built with