Skip to main content

Payment Webhook Subscriptions

The GraphQL standard has first class support for subscriptions. Like GraphQL queries or mutations, GraphQL subscriptions allow you to specify only the data you need. With subscriptions, this data is data pushed to you over time rather than being retrieved immediately.

The subscription specification itself is transport agnostic, meaning that subscriptions could be delivered by any mechanism, including websocket, webhook or email. Stitch has currently opted to support webhooks, but may choose to expand transports to include websockets in the future.

Creating a Subscription​

Each product has a slightly different subscription query. The root of the subscription is either a client or a user, representing the two kinds of tokens on the Stitch platform. However, only client level subscriptions are supported for now.

The subscription query has 3 parameters explained in the below table:

ParameterRequired or OptionalUsage
urlRequiredThe endpoint Stitch will submit the subscription data via a POST request. Due to the sensitive nature of the data being transmitted, the url must use the HTTPS protocol
headersOptionalAllows specifying a set of headers which will be included with the POST request sent to the client's webhook URL
secretRequired for Signed WebhooksA secure passphrase used to sign the request data sent to the client's webhook URL. The same passphrase will be used by the client to decrypt the data and verify authenticity
Did you know ? 🧠

Only a single subscription is required per webhook URL. A single webhook subscription will allow you to receive all payment notification events. Duplicate subscriptions are recognized and ignored by our internal systems. Multiple subscriptions are only necessary when changing the webhook service url or when periodic subscriptions (eg. once a week) are done to ensure subscription health.

Webhook Subscription Examples​

InstantPay Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

InstantPay Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

LinkPay Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

LinkPay Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Refunds Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Refunds Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Disbursements Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Disbursements Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Settlements Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Settlements Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

DirectDeposits Unsigned

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

DirectDeposits Signed

If the subscription is successfully created, the body returned by the request will look like the example shown in the Example Response tab in the widget above, with data being null, and the subscriptionId being contained with the extensions field.

Receiving a Webhook Subscription Event​

Whenever a new event for a subscription is available, it will be posted to the specified target URL, along with the specified headers. For a signed InstantPay subscription, the body should look something like the example below.

Note

The shape of the data in the below example is modelled after the signed InstantPay subscription query above.

{
"data": {
"client": {
"paymentInitiationRequests": {
"node": {
"id": "cGF5cmVxL2Y1YzY5ODRmLWI4MWMtNDg5MS05MjFkLTVkYzJmZjI3MzRkYg==",
"externalReference": "de6b4d3c-af5f-488d-89b9-6f855973bf11",
"state": {
"__typename": "PaymentInitiationRequestCompleted",
"date": "2021-04-09T10:01:25.692Z"
}
},
"subscriptionId": "c3ViL2YxYzVjMTZkLWE0NjQtNDgxYS05NTUyLWUyMjhiYjQzNGE0NAo=",
"eventId": "cGF5cmVxLzdmZmIwNGFkLTExMDQtNDcwNy04NjU5LTI1ZWEzNTZhYjU3Yg==",
"time": "2021-04-09T09:41:36.475Z"
}
}
}
}

Listing Webhook Subscriptions​

To list all the webhook subscriptions your client has active across all the products, you can run the below query. Note that only the products for which you have an active webhook subscription will be in the response.

Unsubscribing from a Subscription​

To unsubscribe from a webhook subscription, you can respond to the webhook payload with the HTTP status code 410, symbolising that the endpoint is no longer available.

Alternatively, you can also submit a mutation to the API with the subscriptionId passed in as an argument. Incase you can't recall the correct subscriptionId, you can use the subscription listing query to identify the subscription you wish to unsubscribe from.

An example is as shown below:

Signed Webhooks​

If a webhook secret is specified, Stitch signs the webhook body by adding a X-Stitch-Signature header to the request. Currently, Stitch supports the HMAC-SHA256 algorithm for this purpose. To learn more about subscribing to signed webhooks across our different products, please refer to the examples section.

Validating the Subscription Payload​

To validate that a given request was genuine, you'll need to calculate the HMAC-SHA256 of the body bytes with the hash being hex (base-16) encoded, and compare it to the value found in the X-Stitch-Signature header. The header will contain two parts, a signature and a timestamp (in seconds since the epoch), in the following format:

X-Stitch-Signature: t=1670320325,hmac_sha256=b3c937fdf0adfa8bc51f18049100c4ea5b32c0305dafbc7429d1da633e6a9648

To protect against cryptographic timing attacks, it is best practice to use a constant-time string comparison to compare the expected signature to each of the received signatures.

To prevent replay attacks, always include the eventId field and time fields in your subscriptions, as these will allow you to disambiguate duplicate events, and also ensure that the signature changes for each event, even if the data is the same.

Webhook IP Addresses

Quite often, clients' systems will only allow whitelisted IPs past their internal firewalls. In order to ensure you're able to receive webhooks from us, we ask that you contact support via Slack or our Support Form πŸ’¬

Sample Code​

The following samples illustrate how to validate the payload for a few different languages. If your language of choice is not listed, the core logic in the samples will still apply.

The steps behind the verification process are as follows:

  1. Extract the timestamp and HMAC SHA256 hash from the X-Stitch-Signature header.
  2. Concatenate the timestamp and the request body, separated by a period (e.g. t + '.' + requestBody).
  3. Compute the hmac_sha256 hash of the concatenated string in (2) above.
  4. Compare the hash with the provided signature, if possible, using a constant-time comparison function (e.g. crypto.timingSafeEqual in NodeJS).
  5. Reject the request if the hash you computed does not match the provided signature.
  6. Your server responds with a 200 or any 2XX response code to indicate the webhook event was received. If a 2XX response is not received, the webhook event will be retried upto 10 times (the retry logic has exponential backoff between attempts).
tip

Ensure that the concatenated string in step (2) above doesn't have any whitespaces, either between the key and value pairs or between the different values. If these extra spaces are there, then the calculated signature won't match the incoming signature. An easy way to ensure this is to deserialize then serialize the JSON string before concatenating.

import crypto from "crypto";

const SECRET = "your hmacSha256Key";
const signatureHeader = req.get("X-Stitch-Signature");
if (!signatureHeader) {
throw new Error("Missing signature");
}

const signature: Record<string, string> = {};
for (const pair of signatureHeader.split(",")) {
const [k, v] = pair.split("=");
signature[k] = v;
}

const hash = crypto
.createHmac("sha256", SECRET)
.update(signature.t)
.update(".")
.update(body)
.digest("hex");

const areEqual = crypto.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(signature.hmac_sha256, "hex")
);
if (!areEqual) {
throw new Error(
`Could not validate signature. Expected request signature ${signature.hmac_sha256} to match ${hash}`
);
}

console.log("Signature is valid!");