Skip to content

Operate · Webhooks

Receive real-time event notifications instead of polling

Qore Payments sends a signed HTTP POST with a JSON payload to your endpoint when an event occurs. Verify the JWT in the Authorization header and the SHA-512 digest of the body before you process it.

HTTPS only. Qore Payments delivers only to a public HTTPS URL, and refuses addresses on private, loopback, link-local, multicast and reserved networks. For local development, expose your server through a public tunnel.

1 Qore Payments Sends a signed POST when an event occurs
2 Your webhook endpoint Checks the digest and signature · returns 2xx
3 Qore Payments Retries any other answer or a timeout 3 times, then drops the event
Qore Payments Your systems

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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. localhost is 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 Authorization JWT, 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 its id through the Transaction Status API returns "found": false. Its merchant_id is yours. Its terminal_id is 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 in 0002.
  • An endpoint subscribed at tenant or account level can't be tested: the test is refused with 422 and Test 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 Requests and Retry-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 with 503 Service Unavailable; try again shortly.
  • A 2xx answer 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 data claim:

    {
      "SHA512": "<digest-value>"
    }
    
    * The <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:approved
transaction-svc:transaction:created
tokenization-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:created
transaction-svc:account:transaction:updated
transaction-svc:transaction:full:created
transaction-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 sub is your merchant ID, verify with your merchant public key, the same key you use to encrypt card data.
  • If sub is 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 sub is 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 2xx answer counts as delivered. Any other status, including a 3xx redirect (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's parent_id is null; match it to your order by order_id. A refund's, capture's, void's, chargeback's or fee's parent_id is 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:declined and transaction-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 event plus payload.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:

  1. Extract the Authorization header (which contains the JWT).
  2. Split authorization header on two parts (Bearer and token)
  3. Decode the JWT header and payload (without verifying yet).
  4. Retrieve the "data" claim from the JWT payload, which contains the SHA-512 digest of the request body.
  5. Hash the raw request body yourself using SHA-512, and encode the result as lowercase hex.
  6. Compare your calculated hash to the SHA512 value inside the data claim.
  7. Verify the JWT's signature using the public key that matches its sub claim (see Which key signs a webhook), with the RS256 algorithm only.
  8. sub is your merchant ID: use your merchant public key.
  9. sub is the Qore Payments tenant ID: use the Qore Payments public key.
  10. Check that exp has not passed (see JWT claims).
  11. 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.

Which key to use

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.