Webhook handler
Follow these intructions to correctly process our webhooks
A webhook handler is a HTTPS endpoint on your server with a URL. You can use one endpoint to handle several different event types at once, or set up individual endpoints for specific events.
Create a webhook endpoint
Set up an HTTPS endpoint that can accept unauthenticated webhook requests with a POST method.
For example using Node.js's Express:
app.post('/shine_webhooks', (req, res) => {
const payload = req.body;
// Add your logic here
});
In this example, the /shine_webhooks
route is configured to accept only POST requests and expects data to be delivered in a JSON payload.
Handle requests from Shine
Your endpoint must be configured to read event objects for the type of event notifications you want to receive. Shine sends events to your webhook endpoint as part of a POST request with a JSON payload.
Each event is structured as an event object with an event
property and the related Shine resource under entity
. Your endpoint must check the event type and parse the payload of each event.
{
"event": "entityType.(create|update)",
"entity": {} // the created/updated entity
}
Your endpoint must quickly return a successful status code (2xx) prior to any complex logic that could cause a timeout.
Built-in retries
Shine webhooks have built-in retry methods for 4xx, or 5xx response status codes. If Shine doesn’t quickly receive a 2xx response status code for an event, the webhook is sent again using an exponential back-off retry strategy. Delivery is retried up to 20 times.
Once all these attempts have failed, the webhook is discarded and won't be sent again.
Secure your webhook handler
Shine sign each webhook it sends to your endpoints by including a signature in the Shine-Signature header. This allows you to verify that the events were sent by Shine, not by a malicious third party.
To verify signatures, you need to retrieve your endpoint’s secret that was sent to you during the webhook registration process.
Shine generates a unique secret key for each endpoint and secrets aren't shared between environments (sandbox vs production).
Verify the signature
Shine generates signatures using a hash-based message authentication code (HMAC) with SHA-512.
- Extract the timestamp and signatures from the corresponding headers
- Prepare the
signedPayload
string created by concatenating the timestamp, the.
character and the request JSON body - Determine the expected signature by computing an HMAC with the SHA512 hash function using the endpoint's secret as the key and the
signedPayload
as the message - Compare the signature in the header with the expected signature.
import { createHmac } from 'crypto';
import { Request, Response } from 'express';
export const verifyWebHookSignature = (req: Request): boolean => {
// 1. Extract the timestamp and signatures from the corresponding headers
const timestamp = req.headers.date; // Date header
const signature = req.headers['shine-signature']; // Shine-Signature header
// 2. Prepare the `signedPayload` string created by concatenating the timestamp, the `.` character and the request JSON body
const { rawBody } = req; // should be the raw unparsed body as JSON parsing can change fields ordering and alter signature
const signedPayload = `${timestamp}.${rawBody}`;
// 3. Determine the expected signature by computing an HMAC with the SHA512 hash function using the endpoint's secret as the key and the `signedPayload` as the message
const hmac = createHmac('sha512', webhookSecret); // webhookSecret is provided by shine
hmac.update(signedPayload);
const expectedSignature = hmac.digest('hex');
// 4. Compare the signature in the header with the expected signature.
return expectedSignature === signature;
};
app.post('/shine_webhooks', async (req: Request, res: Response, next: NextFunction) {
// Verify signature of raw body
if (!verifyWebHookSignature(req)) {
return res.status(400).send('Invalid signature');
}
// Extract the company Profile ID
const { companyProfileId } = req.body.entity;
if (!companyProfileId) {
return res.status(400).send('Invalid data companyProfileId missing');
}
// Add your logic here
});
See a complete example here: https://github.com/shinetools/shine-public-api-example/blob/master/server/routes/webhookHandler.ts
Raw body
Be careful to check the signature. You need to compare it with the raw body before converting it to JSON.
See https://github.com/shinetools/shine-public-api-example/blob/c6e55af7efc0e0368a218b87d73c809af5f72b2e/server/webhookUtils.ts#L6
Prevent replay attacks
A replay attack is when an attacker intercepts a valid payload and its signature, then re-transmits them. To mitigate such attacks, Shine includes a timestamp in the Date header. Because this timestamp is part of the signed payload, it is also verified by the signature, so an attacker cannot change the timestamp without invalidating the signature. If the signature is valid but the timestamp is too old, you can have your server reject the payload.
Shine generates the timestamp and signature once per event. If Shine retries an event (for example, your endpoint previously replied with a non-2xx status code), then the signature and timestamp won't change between retries. You must take this into account when defining if a timestamp is too old.
Handle duplicate events
Webhook endpoints might occasionally receive the same event more than once. We advise you to guard against duplicated event receipts by making your event processing idempotent.
Prevent stale data
As webhooks can be retried, another update can have occured once your server is finally able to process the event. Therefore, we advise you to query the latest version of the related entity upon receiving a webhook.
This will also help mitigate replay attacks.
Handle out of order events
Shine does not guarantee delivery of events in the order in which they are generated. Refer to the previous section to avoid stale data.
Updated 8 months ago