Payments

6.4. 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. What makes subscriptions different, is that 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, email or carrier pigeon. Stitch has currently opted to support webhooks, but may choose to expand transports to include websockets in the future.

Creating a Subscription

To create a webhook subscription, you submit a GraphQL query like the one seen below. As with standard queries, the root of the subscription is either a client or a user, representing the two kinds of tokens on the Stitch platform. Note that within

client or user, you may only have one top level field per subscription. Only client level subscriptions are supported for now though.

The only required parameter for webhook creation is the url parameter. Due to the sensitive nature of the data being transmitted, the url must use the HTTPS protocol.

A handy optional field is available: headers, which allows you to specify a set of headers that will be included with the POST request sent to the url.

If the subscription is successfully created, the body returned by the request will look like the following, with data being null, and the subscriptionId being contained with the extensions field:

1{
2 "data": null,
3 "extensions": {
4 "subscriptionId": "c3ViL2YxYzVjMTZkLWE0NjQtNDgxYS05NTUyLWUyMjhiYjQzNGE0NAo="
5 }
6}

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. The body should look something like the example below. The shape of the data in this example is modelled after the subscription query above:
1{
2 "data": {
3 "client": {
4 "paymentInitiationRequests": {
5 "node": {
6 "id": "cGF5cmVxL2Y1YzY5ODRmLWI4MWMtNDg5MS05MjFkLTVkYzJmZjI3MzRkYg==",
7 "externalReference": "de6b4d3c-af5f-488d-89b9-6f855973bf11",
8 "state": {
9 "__typename": "PaymentInitiationRequestCompleted",
10 "date": "2021-04-09T10:01:25.692Z"
11 }
12 },
13 "subscriptionId": "c3ViL2YxYzVjMTZkLWE0NjQtNDgxYS05NTUyLWUyMjhiYjQzNGE0NAo=",
14 "eventId": "cGF5cmVxLzdmZmIwNGFkLTExMDQtNDcwNy04NjU5LTI1ZWEzNTZhYjU3Yg==",
15 "time": "2021-04-09T09:41:36.475Z"
16 }
17 }
18 }
19}

Unsubscribing

There are two ways of unsubscribing from a webhook subscription, you can respond to the webhook payload with an HTTP status code of 410 GONE , symbolising that endpoint is no longer available, or you can submit a mutation to the API like the one below with the subscriptionId passed in as an argument.

Validating the Subscription Payload

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 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.

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.

An example of how to compute the HMAC in NodeJS can be seen below:

1import crypto from 'crypto';
2
3const SECRET = 'your hmacSha256Key';
4const signatureHeader = req.get('X-Stitch-Signature');
5if (!signatureHeader) {
6 throw new Error('Missing signature');
7}
8
9const signature: Record<string, string> = {};
10for (const pair of signatureHeader.split(',')) {
11 const [k, v] = pair.split('=');
12 signature[k] = v;
13}
14
15const hash = crypto
16 .createHmac('sha256', SECRET)
17 .update(signature.t)
18 .update('.')
19 .update(body)
20 .digest('hex');
21
22const areEqual = crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(signature.hmac_sha256, 'hex'));
23if (!areEqual) {
24 throw new Error(`Could not validate signature. Expected request signature ${signature.hmac_sha256} to match ${hash}`);
25}
26
27if (parseInt(signature.t) < Date.now() / 1000 - 300) /* 5 minutes */ {
28 throw new Error(`Could not validate signature. Timestamp is too old: ${signature.t}`);
29}
30
31console.log('Signature is valid!')

Webhook IP Addresses

Quite often client's firewalls will only allow whitelisted IPs past their internal firewalls. In order to receive webhooks from us we ask that you contact support via the usual support channels on Slack or our Support Form 💬