Preview Release See the table of contents

Working with webhooks

One of BeQue’s key strengths is its ability to push notifications via webhook when there are new results available for complex queries.

To get started, you must:

  1. Configure the URL for your webhook endpoint in your BeQue API dashboard. At least one URL must be configured; for development purposes, more than one URL can be configured. Each URL must be marked as “for production” or “for development” in the dashboard. All configured URLs will be invoked for all installed queries. (For local development, we recommend running your back-end via ngrok or similar tunneling tool so that your webhook endpoint is reachable).
  2. Grab the webhook endpoint secret. This is used by BeQue to sign its invocations of your webhook; you should use our SDK to validate this signature on each webhook invocation. (See below for details).

BeQue will make best effort to deliver notifications in a timely manner. Notifications that receive an HTTP 2xx status code in response will be considered successfully delivered; notifications that receive any other status codes will be retried at increasing time intervals until either three retries have failed, or a supersceeding notification would have been sent regardless. Webhooks that receive no successful notifications over a 24-hour timespan will be disabled; you can re-enable them in the BeQue dashboard.

Installing queries for notification

To install a query for future notification, define the query and use Webhook.listen() to install it.

import { Trade } from "@beque/nft";
import { Webhook } from "@beque/push";
import type { Listener } from "@beque/push";

// Look for $1M trades or better
const bigTradeQuery = Trade.query().where("usdAmount", ">=", 1_000_000);

// Listen to this query. BeQue will call our Webhook HTTP endpoint
// when new blocks are mined and our query has relevant new results.
const listener: Listener = await Webhook.listen(bigTradeQuery);

The returned listener instance can be used to work with, and disable, future notifications for this query. In particular, listener.id contains a unique identifier for this installed query that you will want to use elsewhere. Most likely, you will want to store it in your service’s database. You can also see and remove installed queries from the BeQue API dashboard.

interface Listener {
  /** A beque-provided unique identifier */
  id: string;

  /** A user-provided unique identifier */
  userId?: string;
}

By default, calling Webhook.listen(...) creates a production query; production webhook URLs will be invoked. You can create a development query by sending options to Webhook.listen(...):

const listener = await Webhook.listen(bigTradeQuery, { development: true });

In addition, you can provide your own user identifier for installed queries:

const listener = await Webhook.listen(query, { userId: "big-trade-query-01" });

The provided userId must be unique; if the same userId is re-used, the call to listen(...) will result in a no-op and return an existing listener.

Verifying webhook invocations

Before processing a webhook event, you should verify its signature to convince yourself that the BeQue service did indeed send the notification. BeQue passes a signature in the BeQue-Signature HTTP header.

Here’s example code assuming you’re using Express:

import express from "express";
import bodyParser from "body-parser";
import { Event } from "@beque/push";

// get this from your BeQue API dashboard
// ...and don't *actually* hard-code it in your own code
const webhookSecret = "whsec_...";

// Use express.js to listen for webhooks
const app = express();

// And use bodyParser to allow us to access the body of the request
// as a nodejs buffer
const parser = bodyParser.raw({ type: "application/json" });

// An example webhook route
app.post("/webhook", parser, (request, response) => {
  const signature = request.headers["beque-signature"];

  let event: Event;

  try {
    event = new Event(request.body, signature, webhookSecret);
  } catch (err) {
    // reject this webhook invocation; it's not valid
  }

  // process the event here...
});

Processing webhook events

The Event type describes an update to an installed query:

interface EventBase {
  /** The listener to which this event applies */
  listener: Listener;
}

interface UpdateEvent extends EventBase {
  /** The nature of the event; currently, there's only one kind */
  kind: "update";

  /** New results associated with this update */
  results: Result[];
}

/** In the future, there will be multiple event types */
type Event = UpdateEvent;

This can be used to take action when new results are available:

// process the event here...
for (const nft of event.results) {
  console.log(`${nft.name} was *just* purchased for ${nft.usdAmount}`);
  // do something cleverer here
}

Because webhooks on a given back-end instance may be invoked by queries installed from development or other instances, it’s generally comparing the event.listener.id (or event.listener.userId) to known-good identifiers before processing an event. Unknown identifiers can (and probably should) be safely ignored.

Removing queries for notification

To stop listening to a specific query, call Webhook.stopListening(...):

await Webhook.stopListening(listener);