Verify Transactions with WebHooks

It is a good practice to verify the state of a transaction independently from your app, i.e. via your backend. With WebHooks you get notified about transaction events.

Register a WebHook

You can edit WebHooks in the Gateway Manager.

Go to the account settings by clicking your email in the top right corner. In the section 'WebHooks', you can create, edit, and delete WebHooks.

After the WebHook is set up, you will receive event callbacks when the status of your transaction changes.

Handle Incoming Event Data

For an APPROVED transaction, a transaction.succeeded event is fired both for CHARGE and REFUND transactions. This is what the body of the HTTP POST request to your registered WebHook URL will look like:

post Request
	"identifier": "cf30cfba-e62a-4903-99ff-ea3dbac52c8a",
	"created": "2013-07-09 12:12:01",
	"type": "transaction.succeeded",
	"transaction": {
		"identifier": "1c9b1add-0fec-4e25-8ad9-f314e8bf0a80",
		"groupIdentifier": "a1afe57f-520d-44cc-8b29-0e3800453481",
		"customIdentifier": "myX.12390.12309",
		"referencedTransactionIdentifier": null,
		"amount": 3.14,
		"currency": "EUR",
		"created": "2013-09-03 12:38:36",
		"status": "APPROVED",
		"subject": "A bunch of flowers",
		"type": "CHARGE",
		"mode": "TEST"
		"statusDetails": {…},
		"locationDetails": {…},
		"processingDetails": {…},
		"paymentDetails": {…},
		"receiptDetails": {…},
		"reader": {…}
We send the raw JSON with a Content-Type of application/json. Please make sure that you access the HTTP body accordingly. Depending on the language that you implement your WebHook in, this might be considerably different from the way you access POST parameters sent by HTML forms (those usually have a Content-Type of application/x-www-form-urlencoded).

The JSON contains the event with its identifier (referred to as eventIdentifier), type and created date (in UTC). transaction already contains all details about the transaction, like your transactionIdentifier.

For a transaction that results in a status of DECLINED, ABORTED or ERROR a transaction.failed event is fired, also containing all the details in the transaction dictionary:

post Request
	"identifier": "cf30cfba-e62a-4903-99ff-ea3dbac52c8a",
	"created": "2013-07-09 12:12:01",
	"type": "transaction.failed",
	"transaction": {…}
Should your app during a transaction crash or lose the connection to the platform for any reason, your transaction will remain in the status PENDING for about 15 minutes until it runs into a timeout. Its status will then automatically turn into ERROR and you will receive a transaction.failed event.

Respond to the Incoming Event Data

Respond with a HTTP status code of 200, 201, or 202 to indicate that your server received and processed the call. If you respond with another status code, we consider the event delivery unsuccessful. The platform then tries to send you this transaction event again

  1. after 30 seconds
  2. each hour - for up to 3 days

to make absolutely sure you do not lose information about your transactions.

Verify Signature to Confirm Authenticity

In theory, anyone can call your WebHook endpoint and send event and transaction data. To make sure that the event happened and the request is coming from Payworks, we recommend to verify the signature embedded in the WebHook request.

The signature is based on asymmetric cryptography -- we sign every request using a private key and a client can verify the signature using a public key, loaded in a secure way from our public endpoint.

The Public Key

Download the public key using an API endpoint:

get /signingKeys/public Request
    "kid": "<uuid>",
    "value": "<key>",
    "alg": "RSA"

The kid field identifies the key, the key itself is Base64 encoded. While we don't plan on rotating keys in the near future, for security reasons we reserve the right to change it.

JWT Signature

Every request contains an authorization header containing a JWT token: Authorization: Bearer <token>. The token structure adheres to the JWT standard:

    "kid": "<uuid>",
    "alg": "RS256",
    "typ": "JWT"

    "iat": <timestamp>,
    "iss": "payworks",
    "digest": "<digest>",
    "digestAlgorithm": "SHA-256"

RS256Hash(base64Encode(header) + “.” + base64Encode(claims), private_key)

The digest claims field contains a SHA-256 hash of the request body, to verify that the body content was not modified.

Verification Steps

  1. Load the public key using the described endpoint and cache it.
  2. Extract the JWT token from the WebHook request.
  3. Compare the kid header value with the downloaded public key ID.
  4. Verify the signature of the request using the public key.
  5. Extract the digest claims value.
  6. Calculate a SHA-256 hash of the request body.
  7. Compare the hash value with the digest value -- for a non-tampered request, the values must be equal.
We recommend to use an external library for parsing and verifying the JWT token.