Best practices

Review these best practices to make sure your webhooks remain secure and function well with your integration

Duplicate event handling

Webhook endpoints may receive the same event more than once, which can result in duplicated event receipts. To prevent this, it's important to make your event processing idempotent. One way to achieve this is by logging the events that you have already processed, and then not processing any events that have already been logged.

Specifically listening to event types your integration requires

To optimize the performance of your webhook endpoints, it's recommended that you configure them to receive only the types of events that are required by your integration. Listening for extra events, or all events, can put undue strain on your server.

You can change the events that a webhook endpoint receives in the PaySG Dashboard.

Exempt webhook route form CSRF protection

For applications using Rails, Django, or another web framework, your site might automatically check that every POST request contains a CSRF token as a security feature to protect you and your users from cross-site request forgery attempts. However, this might also prevent your site from processing legitimate events. In such cases, you might need to exempt the webhooks route from CSRF protection.

Verify events are sent from PaySG

You should verify webhook signatures to confirm that received events are sent from PaySG. PaySG signs webhook events that are sent to your endpoints by including a signature in each event's PaySG-Signature header. This allows you to verify that the events were sent by PaySG, not by a third party.

To verify webhook signatures, developers can follow these steps:

  1. Extract the signature from the HTTP header called PaySG-Signature

  2. The value of the header has the format t=timestamp, v1=signature. Extract the timestamp and the signature

  3. Concatenate the timestamp and the JSON-stringified HTTP payload, separated by a . to form the signature payload

  4. Use HMAC-SHA256 and the symmetric key provided to you by PaySG to verify that the signature is valid

Retrieving your endpoint's secret

For now, they can be obtained through direct request to the PaySG Team. The PaySG team will provide the secret to you when we successfully help you register it.

The secret is a unique value that is used to sign webhook events sent to your endpoint. You will need to use this secret to verify the signature of incoming webhook events.

Preventing replay attacks

To prevent replay attack, PaySG includes a timestamp in the PaySG-Signature header. This timestamp is part of the signed payload and is verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, your e-service should reject the payload.

You can consider having a tolerance between the timestamp included in the header and the current time.

Note that if PaySG retries an event (for example, if your endpoint previously replied with a non-2xx status code), a new signature and timestamp will be generated for the new delivery attempt.

Verifying signatures manually

You can also create a custom solution by following these steps:

Step 1: Extract the timestamp and signatures from the header

The PaySG-Signature header included in each signed event contains a timestamp and one or more signatures that you must verify. The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature (or signatures). To prevent downgrade attacks, ignore all schemes that are not v1.

t=, // timestamp
v1= // event-id

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

  • The timestamp (as a string) t

  • The character .

  • The actual JSON payload (i.e., the request body).

Step 3: Determine the expected signature

Compute an HMAC with the SHA-256 hash function. Use the endpoint's signing secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures

Compare the signature in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance. To protect against timing attacks, use a constant-time-string comparison to compare the expected signature to each of the received signatures.

Last updated