Introduction
Webhooks allow your application to receive real-time notifications from Qore Payments whenever specific events occur. Instead of polling APIs, you can set up a webhook endpoint to automatically receive updates when important activities happen.
Webhooks enable event-driven workflows, such as transaction status updates or system notifications, without the need for manual polling.
How Webhooks Work
When an event occurs (for example, a transaction is processed), Qore Payments sends an HTTP POST request to your registered webhook URL. The request contains a JSON payload describing the event.
Each webhook request is signed using JWT (JSON Web Token) for security and integrity verification.
You must set up an endpoint on your server to receive, validate, and process these webhook notifications.
Configure webhooks in the portal
Webhooks are self-service. Open Webhooks in the Qore Payments merchant portal:
- Select Add endpoint, then choose the shop, enter your endpoint URL and a label, and pick the events to send. An endpoint subscribes to events from one event service:
- Transaction processed: the Transaction Processed events. The picker groups them under Any transaction type and By transaction type. An event in one group and its twin in the other fire for the same change, so subscribing to both sends two deliveries (see Delivery and Retry Policy).
- Transaction created:
transaction-svc:transaction:created. - Payment method tokenized: the Payment Method Tokenized event.
- Select an endpoint in the list to see its Deliveries (every attempt, with what was sent and what came back), its Subscribed events and its Settings. Not every portal role can read the delivery log: for one that can't, Deliveries shows Delivery log not available, and the Test endpoint tab still shows the full exchange of a test.
- Use Edit to change the shop, the URL, the label or the events; Subscribed events also changes the events in place. Settings pauses, resumes or deletes the endpoint, and shows which key signs each kind of event.
- Use Send test event on the endpoint, or the Test endpoint tab next to Endpoints, to send a signed sample before real events arrive. See Test your endpoint.
Adding, editing, pausing, resuming and deleting endpoints needs the Manage webhook endpoints permission. Without it, you can still see the endpoints, their Deliveries and Subscribed events, and send test events.
The endpoint URL must be a public https:// URL:
- An address on a loopback, private (RFC 1918), link-local, unique-local or multicast network is refused, and so is one in a shared or reserved IPv4 range (for example CGNAT
100.64.0.0/10) or an IPv6 transition address (NAT64, 6to4, Teredo). This is checked both when you save the endpoint and when the URL's host name is resolved for a delivery.localhostis never reachable, because Qore Payments sends from its own servers, not from your machine. - For local development, expose your server through a public HTTPS tunnel and use the tunnel's URL.
- Redirects are not followed. Point the endpoint at the URL that answers.
If you cannot access the portal, contact the Qore Payments Support Team.
Support contact information is available through your client portal or support@qore.com.
Test your endpoint
The Test endpoint tab sends one sample of the event you pick from those your endpoint subscribes to, and shows the request Qore Payments sent and the answer your endpoint returned.
- The sample is built and signed exactly like a real delivery: the same
AuthorizationJWT, the same body shape and the same signing key. - Every test delivery carries the header
X-Webhook-Test: true. Real deliveries never carry it, so your endpoint can tell the two apart. - A transaction sample describes an invented transaction with the reference
webhook-test: looking up itsidthrough the Transaction Status API returns"found": false. Itsmerchant_idis yours. Itsterminal_idis the endpoint's own terminal for a terminal endpoint; for a shop or merchant endpoint it is the newest active terminal of that shop or merchant, or an invented ID if there is none. A tokenization sample describes an invented saved Visa card ending in0002. - An endpoint subscribed at tenant or account level can't be tested: the test is refused with
422andTest events are not available for <event>. - A test is sent once and never retried. If your role can read the delivery log, the test also appears in Deliveries.
- You can send one test per endpoint every 5 seconds. A faster request is refused with
429 Too Many RequestsandRetry-After: 5, and the Send test event button counts down until you can send again. If the limit can't be checked, the test is refused with503 Service Unavailable; try again shortly. - A
2xxanswer only proves that your endpoint answered. Check that your endpoint verifies the signature and the digest (see below) and rejects a request that fails either check.
Security and Signature Verification
Every webhook request from Qore Payments includes a JWT token in the Authorization header, as Bearer <JWT>.\
The JWT is signed with RS256 and must be verified to ensure the authenticity of the webhook.
Signature details:
-
The JWT payload contains a
dataclaim:* The{ "SHA512": "<digest-value>" }<digest-value>is the SHA-512 hash of the webhook request body, encoded as lowercase hexadecimal (128 characters). It is not Base64. * Hash the raw request body exactly as you received it, before you parse or re-serialise the JSON. Your digest must match the one in the JWT payload. * The JWT must also be cryptographically verified using the correct public key: see Which key signs a webhook.
Which key signs a webhook
The key depends on the event, and the JWT's sub claim tells you which one was used:
| Events | Signed with | sub claim |
|---|---|---|
Every event you can subscribe to in the portal: every Transaction Processed event, for example transaction-svc:transaction:purchase:approvedtransaction-svc:transaction:createdtokenization-webhook:payment-method:tokenized |
Your merchant private key | Your merchant ID |
Only on a business-account or tenant-level feed that Qore Payments sets up for you:transaction-svc:account:transaction:createdtransaction-svc:account:transaction:updatedtransaction-svc:transaction:full:createdtransaction-svc:transaction:full:updated |
The Qore Payments private key | The Qore Payments tenant ID |
An endpoint you add in the merchant portal only ever receives events signed with your merchant key, so its sub is always your merchant ID.
To choose the key, decode the JWT without verifying it and read sub:
- If
subis your merchant ID, verify with your merchant public key, the same key you use to encrypt card data. - If
subis the Qore Payments tenant ID, verify with the Qore Payments public key. This happens only on a business-account or tenant-level feed that Qore Payments sets up, and Qore Payments gives you the key and the tenant ID with that feed. - If
subis anything else, reject the webhook.
JWT claims
| Claim | Value |
|---|---|
sub |
The ID of the key owner: your merchant ID or the Qore Payments tenant ID |
upn |
The same value as sub |
iat |
When the JWT was signed, in seconds since the epoch |
exp |
iat + 300: the JWT is valid for 5 minutes. A decline webhook held while a retry may still follow is signed when the decline is processed and sent up to 15 minutes later; its exp is its planned send time + 300 + 3600, also when it is sent earlier |
jti |
A unique ID for the JWT |
data |
{"SHA512": "<digest-value>"}, the digest of the body |
The JWT is signed with RS256 and an RSA 2048-bit key. It has no iss, aud or kid claim, so don't require them. The JWT is signed once per delivery and sent again unchanged on every retry, with the same jti, so don't reject a jti you have already seen. Check exp with a small allowance for clock skew, for example 60 seconds.
Public key format
Your merchant public key, and the Qore Payments public key, are Base64-encoded X.509 SubjectPublicKeyInfo (DER) with no PEM header. Most JWT libraries expect PEM, so wrap the value first:
// publicKeyBase64 is the key exactly as Qore Payments gives it to you
const pem = '-----BEGIN PUBLIC KEY-----\n'
+ publicKeyBase64.match(/.{1,64}/g).join('\n')
+ '\n-----END PUBLIC KEY-----\n';
Delivery and Retry Policy
Every delivery is a POST with these headers:
| Header | Value |
|---|---|
Authorization |
Bearer <JWT> |
Content-Type |
application/json |
Accept |
text/plain |
X-Webhook-Test |
true, on test deliveries only |
Standard HTTP headers such as Host, Content-Length and User-Agent are sent too; don't rely on their values.
- Only a
2xxanswer counts as delivered. Any other status, including a3xxredirect (redirects are not followed), is a failed attempt. - Each attempt must finish within 15 seconds, from connecting to the last byte of your answer. An answer still arriving after 15 seconds is a failed attempt.
- A failed delivery is retried 3 times with exponential backoff: about 1, 2 and 4 seconds after the previous attempt, each wait varying by up to 50%. That is 4 attempts in all when your endpoint answers quickly.
- If the 4 attempts take more than 60 seconds together, because your endpoint doesn't answer or takes more than about 12 seconds to answer each one, the whole delivery starts again, up to 3 more times. An endpoint that never answers receives up to 16 attempts over about 4 minutes, so answer within a few seconds.
- After the last retry fails, Qore Payments makes no further attempts, so use the Transaction Status API to recover an outcome you missed. Every attempt appears in the endpoint's Deliveries in the portal, for a role that can read the delivery log.
- Deliveries are not ordered. Don't assume the events for one transaction arrive in the order they happened.
- You see only the latest attempt of an order. When a declined payment can still be retried, for example on another payment provider, its decline webhook waits up to 15 minutes. It is dropped if a retry approves or declines in the meantime, because you then get that retry's webhook instead. It goes out at once if the retry's session expires or the retry is cancelled before it was approved, unless another retry is still running. Other events, such as a retry's
created, still arrive. A retry'sparent_idisnull; match it to your order byorder_id. A refund's, capture's, void's, chargeback's or fee'sparent_idis the payment it acts on. A held decline appears in the endpoint's Deliveries once it is sent; a dropped one never does. - The same change can be delivered more than once. Some state changes also fire a general event and a type-specific one, for example
transaction-svc:transaction:declinedandtransaction-svc:transaction:purchase:declined. An endpoint subscribed to both gets two deliveries.
Ensure that your webhook endpoint:
- Is highly available.
- Quickly responds to POST requests (recommended within 2 seconds, and never later than 15).
Best Practices
- Always verify the JWT signature and the SHA-512 digest before processing the event.
- Log webhook payloads and headers for audit and troubleshooting purposes.
- Implement idempotency to safely handle duplicate webhook deliveries, for example by keying on
eventpluspayload.id. - Respond with an HTTP 2xx status as quickly as possible.
- Secure your webhook endpoints with HTTPS.
JWT Signature Verification – Flow Description
When your server receives a webhook request from Qore Payments, you must:
- Extract the
Authorizationheader (which contains the JWT). - Split authorization header on two parts (Bearer and token)
- Decode the JWT header and payload (without verifying yet).
- Retrieve the "data" claim from the JWT payload, which contains the SHA-512 digest of the request body.
- Hash the raw request body yourself using SHA-512, and encode the result as lowercase hex.
- Compare your calculated hash to the
SHA512value inside thedataclaim. - Verify the JWT's signature using the public key that matches its
subclaim (see Which key signs a webhook), with the RS256 algorithm only. subis your merchant ID: use your merchant public key.subis the Qore Payments tenant ID: use the Qore Payments public key.- Check that
exphas not passed (see JWT claims). - If the digest matches, the signature is valid and the JWT has not expired → accept the webhook.
If any of these steps fail → reject the webhook (return 401 Unauthorized).
Verify a webhook
Check the digest, then the signature, and answer 2xx only when both pass. Any other answer is a failed attempt and is retried, as described in Delivery and Retry Policy.
sequenceDiagram
participant Gateway
participant Server as Your webhook endpoint
Gateway->>Server: Webhook event, JWT in Authorization
Server->>Server: Decode the JWT without verifying it
Server->>Server: SHA-512 the raw body, compare with data.SHA512
Server->>Server: Verify the signature with the key for sub
alt Both checks pass
Server-->>Gateway: 2xx
Server->>Server: Process the event
else Either check fails
Server-->>Gateway: 401 Unauthorized
end
Summary
- The digest comparison ensures payload integrity (body wasn't tampered).
- The JWT signature ensures origin authenticity (Qore Payments sent it, not someone else).
- Both must pass.