Webhooks are automated messages sent from Hyperline when something happens on our system. They have a specific payload for each action they notify and are sent to a unique URL, an HTTPS endpoint on your server. Webhooks are a great way to integrate your application with Hyperline as you can fine-tune product flows and build deeper integrations based on your needs, with little development work on your end.

Add a webhook endpoint

To start listening to webhook message sent by Hyperline, go to your webhooks settings in the app, click on “Add Endpoint”, provide the termination URL that you control, and select the event types you want to listen to. You can add as many URLs, if you want to segregate the events you want to listen to.

That’s it! You will now receive webhook calls when the event occurs on the Hyperline system.

Event types

A complete list of the event types and the shape of the event payload can be find in the product Webhooks page, on the “Event Catalog” tab.

Here is a preview of the messages you can be notified:

  • customer.created, customer.updated, customer.archived
  • subscription.created, subscription.activated, subscription.paused, subscription.cancelled, subscription.voided, subscription.errored, subscription.charged
  • invoice.ready, invoice.grace_period.started, invoice.settled, invoice.errored
  • credit_note.ready, credit_note.settled
  • checkout.completed
  • payment_method.created, payment_method.activated, payment_method.deleted
  • wallet.credited, wallet.debited
  • daily_analytics.ready

Consuming webhooks

Webhook calls will always be POST HTTPS request contains a JSON body with this format:

{
  "event_type": "<group>.<event>",
  "data": {}
}

Your endpoint must quickly return a 2xx (status code 200-299) response prior to any complex logic that could cause a timeout (max 15s).

Another important aspect of handling webhooks is to verify the signature and timestamp when processing them.

Testing events

The easiest way to be more confident in your endpoint configuration is to start receiving events as quickly as possible. The “Testing” tab is here to help you to send example events to your endpoint.

After sending an example event, you can click into the message to view the message payload, all of the message attempts, and whether it succeeded or failed.

Additionally, the Logs section is here to help you have a complete view of past call attempts with their date, status, and payload.

Replaying events

If you want to replay a single (or multiple) events, you can find the message from the UI, open the options menu next to any of the attempts, and click on “Resend”.

It’s a great help if your service had downtime or if your endpoint was misconfigured.

Retry schedule

Each message is attempted based on the following schedule, where each period is started following the failure of the preceding attempt:

  • Immediately
  • 5 seconds
  • 5 minutes
  • 30 minutes
  • 2 hours
  • 5 hours
  • 10 hours
  • 10 hours (in addition to the previous)

If an endpoint is removed or disabled delivery attempts to the endpoint will be disabled as well.

Handling duplicate events

We strongly 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 occur 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.

Verify webhooks

Verifying webhooks is an important part of the consumption. Because of the way webhooks work, attackers can impersonate services by simply sending a fake webhook to an endpoint. This is a potential security hole for your application.

In order to prevent it, every webhook and its metadata is signed with a unique key for each endpoint. This signature can then be used to verify the webhook indeed comes from Hyperline, and only process it if it is.

Another potential security hole is what’s called replay attacks. A replay attack is when an attacker intercepts a valid payload (including the signature), and re-transmits it to your endpoint. This payload will pass signature validation, and will therefore be acted upon.

To mitigate this attack, a timestamp (webhook-timestamp header) is included in every request for when the webhook attempt occurred. We recommend you reject webhooks with a timestamp that is more than five minutes away (past or future) from the current time.

Verifying signatures

Each webhook call includes three headers with additional information that are used for verification:

  • webhook-id: the unique message identifier for the webhook message. This identifier is unique across all messages but will be the same when the same webhook is being resent (e.g. due to a previous failure).
  • webhook-timestamp: timestamp in seconds since epoch.
  • webhook-signature: the Base64 encoded list of signatures (space delimited).

Constructing the signed content

The content to sign is composed by concatenating the id, timestamp, and payload, separated by the full-stop character (.). In code, it will look something like:

signedContent = "${webhook_id}.${webhook_timestamp}.${body}"

Where body is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying.

Determining the expected signature

HMAC with SHA-256 is used to sign webhooks.

So to calculate the expected signature, you should HMAC the signed_content from above using the base64 portion of your signing secret (this is the part after the whsec_ prefix) as the key. For example, given the secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw you will want to use MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

Here is an example of how you can calculate the signature in Node.js:

const crypto = require("crypto");

const signedContent = `${webhook_id}.${webhook_timestamp}.${body}`;
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

// Need to base64 decode the secret
const secretBytes = new Buffer(secret.split("_")[1], "base64");
const signature = crypto
  .createHmac("sha256", secretBytes)
  .update(signedContent)
  .digest("base64");

console.log(signature);

This generated signature should match one of the ones sent in the webhook-signature header.

This header is composed of a list of space-delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one. Though there could be any number of signatures. For example:

v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=

Make sure to remove the version prefix and delimiter (e.g. v1,) before verifying the signature.