Blog Webhooks
Updated over a week ago

Introduction

The webhooks feature in Hashnode allows customers to configure webhooks to receive events for certain actions related to their publication.

Events

There are currently six events you can listen to.

  • post_published

    {
    "metadata": {
    "uuid": "5e74b628-162a-4fe7-8130-1d5db664600d"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "post": {
    "id": "65b24b924bf2eb3c5d3f01c8"
    },
    "eventType": "post_published"
    }
    }

  • post_deleted

    {
    "metadata": {
    "uuid": "822adb5a-2d1b-4650-932f-36b2bc3a298e"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "post": {
    "id": "65b24b924bf2eb3c5d3f01ca"
    },
    "eventType": "post_deleted"
    }
    }

  • post_updated

    {
    "metadata": {
    "uuid": "032812a8-19d0-403f-b673-23af579f407d"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "post": {
    "id": "65b24b924bf2eb3c5d3f01c9"
    },
    "eventType": "post_updated"
    }
    }

  • static_page_published

    {
    "metadata": {
    "uuid": "5173ae67-828d-4b0f-ac60-9a2442742277"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "staticPage": {
    "id": "65b24b924bf2eb3c5d3f01cb"
    },
    "eventType": "static_page_published"
    }
    }

  • static_page_edited

    {
    "metadata": {
    "uuid": "e1f6b914-89a4-42a9-9835-d7f8b4a0f4e4"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "staticPage": {
    "id": "65b24b924bf2eb3c5d3f01cc"
    },
    "eventType": "static_page_edited"
    }
    }

  • static_page_deleted

    {
    "metadata": {
    "uuid": "61e5650d-4984-422f-b9ea-6684c5fd17aa"
    },
    "data": {
    "publication": {
    "id": "5f5f88cc93fd9275f04d3a5f"
    },
    "staticPage": {
    "id": "65b24b924bf2eb3c5d3f01cd"
    },
    "eventType": "static_page_deleted"
    }
    }

If you need more events please reach out to support or create a feature request.

ℹ️ Leverage our public GraphQL API to get additional information about the referenced data types in the webhooks payload.

Configuration

To configure webhooks for your publication, follow these steps:

Step 1: Go to your publication's dashboard.

Step 2: Navigate to the Webhooks section.

Step 3: Click on Add New Webhook

Step 4: In the first step, specify the URL where you want to receive the webhook events.

Step 5: Select the type(s) of event(s) you want to receive notifications for.

Step 6: Click on Create.

ℹ️ We guarantee at least once delivery. Make sure to duplicate requests if needed. You can use the metadata.uuid for that.

Features

Signing

We send a signature with each request. You can use this signature to verify that Hashnode is the sender of the request and prevent against replay attacks (someone trying to just reuse the same signature from a previous request).

We send the signature in the x-hashnode-signature header. It includes a timestamp (prefixed with t=) and the actual signature (prefixed with the signature version – currently only 1 – v1=). Both are delimited by a comma (,).

Example:

t=1696259614983,v1=3e8bc8d55b37650920ff90e2df79c6ea50be260507ae21bb49e79ebd246c9f8c

ℹ️ We follow Stripe’s signature approach. Check out how you can verify the signature manually.

TypeScript code for validating the signature:

import crypto from 'crypto';

const MILLISECONDS_PER_SECOND = 1_000;
const SIGNATURE_VERSION = '1';

/**
* Parses the signature header and returns the timestamp and signature.
*
* @example
* parseSignatureHeader('t=1629780000,v1=0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b')
*/
export function parseSignatureHeader(header: string) {
const parts = header.split(',');
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
const signature = parts.find((part) => part.startsWith(`v${SIGNATURE_VERSION}=`))?.split('=')[1];

if (!timestamp || !signature) {
return { success: false as const, data: null };
}

return { success: true as const, data: { timestamp: parseInt(timestamp, 10), signature } };
}

export type CreateSignatureOptions = {
/**
* The timestamp of the signature.
*/
timestamp: number;
/**
* The payload to be signed.
*/
payload?: Record<string, unknown>;
/**
* The secret of your webhook (`whsec_...`).
*/
secret: string;
};

export function createSignature(options: CreateSignatureOptions) {
const { timestamp, payload, secret } = options;
const signedPayloadString = `${timestamp}.${payload ? JSON.stringify(payload) : ''}`;
return crypto.createHmac('sha256', secret).update(signedPayloadString).digest('hex');
}

export type ValidateSignatureOptions = {
/**
* The content of the signature header.
*/
incomingSignatureHeader: string | null;
/**
* The payload that was signed.
*/
payload?: Record<string, unknown>;
/**
* The secret of your webhook (`whsec_...`).
*/
secret: string;
/**
* The number of seconds that the timestamp can differ from the current time before the request is rejected. Provide 0 to disable the check.
*/
validForSeconds?: number;
};

export type ValidateSignatureResult = { isValid: true } | { isValid: false; reason: string };

/**
* Checks the signature validity and whether the timestamp is within the validForSeconds window.
*/
export function validateSignature(options: ValidateSignatureOptions): ValidateSignatureResult {
const { incomingSignatureHeader, payload, secret, validForSeconds = 30 } = options;

if (!incomingSignatureHeader) {
return { isValid: false, reason: 'Missing signature' };
}

const { success: isParsingSuccessful, data: parseSignatureHeaderData } =
parseSignatureHeader(incomingSignatureHeader);
if (!isParsingSuccessful) {
return { isValid: false, reason: 'Invalid signature header' };
}

const { timestamp: incomingSignatureTimestamp, signature: incomingSignature } = parseSignatureHeaderData;

const signature = createSignature({ timestamp: incomingSignatureTimestamp, payload, secret });
let isSignatureValid = compareSignatures(signature, incomingSignature);
if (!isSignatureValid) {
return { isValid: false, reason: 'Invalid signature' };
}

if (validForSeconds !== 0) {
const differenceInSeconds = Math.abs((Date.now() - incomingSignatureTimestamp) / MILLISECONDS_PER_SECOND);

const isTimestampValid = differenceInSeconds <= validForSeconds;
if (!isTimestampValid) {
return { isValid: false, reason: 'Invalid timestamp' };
}
}

return { isValid: true };
}

function compareSignatures(signatureA: string, signatureB: string) {
try {
return crypto.timingSafeEqual(Buffer.from(signatureA), Buffer.from(signatureB));
} catch (error) {
return false;
}
}

Your webhook secret is available in your publication dashboard in Edit Webhook. You can replace or unhide your secret from the dashboard.

Sending Test Requests

You can send test events to each configured webhook to ensure that they are set up correctly. To send a test request, click on Test on the configured webhook.

This triggers a request for every specified event. The payload structure is the same as for actual triggers but the data is random.

ℹ️ You can create an easy-to-use webhook destination using https://webhook.site. Just copy the unique webhook URL from there.

Last Status

Each webhook indicates the last status of your request. There are three different states:

Grey - Not sent yet: Your request hasn’t been sent

Red - Error: A response or request error happened

Green - Successful: The last request was successful

History

The history feature allows you to view a record of all past events that were sent to each webhook. To see the history click on your configured webhook.

You see all requests being sent, wether or not they were sent as a test or resent and the request and response (if available):

Resending Webhook Events

If you need to resend a webhook request, you can do so from the history section. Select the desired request and click on the Resend button and the exact event will be sent to your configured webhook (only the signature will differ due to the updated timestamp).

Retries

If a request fails we retry the request up to 3 times.

When will a request fail?

  • Response code > 299

  • Request failed

  • more than 3 redirects occur

ℹ️ Each event includes a metadata.uuid. This identifies a request and will be the same for retried requests.

Example Use Cases

Here are a few examples of how you can leverage webhooks to enhance your Hashnode experience:

Invalidate Cache of static page

If you use Hashnode as a headless CMS and cache your static pages, you can use webhooks to revalidate or invalidate your site when new events occur.

GitHub Bio Update

You can use webhooks to automatically update your GitHub bio with your latest published posts. By listening to webhooks and triggering a bio update, you can showcase your most recent content to your GitHub profile visitors.

Trigger Build of Static Page

If you have a fully static page that needs to be rebuilt whenever a new post is published or updated, you can use webhooks to trigger the rebuild process. By configuring a webhook to listen for post publication and update events, you can ensure that your static page is always up to date.

Cross-Post

You want to publish your content somewhere else as well? Trigger the cross posting (and updates and deletions) via webhooks.

Notifications

You want to notify others (e.g. on social media or via email) if there is new/changed content? You can do this with the power of webhooks.

Conclusion

The webhooks feature in Hashnode provides a powerful way to receive notifications for specific events related to your publication. By configuring webhooks, you can integrate Hashnode with other services and automate various actions based on the events you receive.

For more information and detailed instructions on how to configure webhooks, refer to the official Hashnode documentation.

Did this answer your question?