Webhooks

Webhooks allow you to be notified of events occuring on Zeplo. For the events you configure, Zeplo will send a POST request to your endpoint containing data related to the event.

Webhooks can be configured on the Zeplo console, and can be found in the Workspace > Settings > Webhooks page.

Verifying Signatures

Zeplo’s webhooks are sent with a signature the destination server can use to verify that the event came from the Zeplo platform and not a third party or malicious system. It is strongly recommended that webhook consumers verify these signatures before processing each event.

Obtaining the Secret

When a webhook subscription is created, a strong unique secret is generated and provided in the console. This secret is used to sign webhook payloads sent as part of this subscription. The signature is then delivered along with each payload under the X-Zeplo-Signature header.

The Signature

The X-Zeplo-Signature header included with each webhook event delivery contains one or more signatures. Multiple signatures may be present to allow for a zero-downtime secret rotation.

The current signature version is v1 and is computed as an HMAC of the payload body using a SHA-256 hash function. This is performed for each signing secret and the results are concatenated using comma separation. An example signature is shown below:

X-Zeplo-Signature:
v1=f03de6f61df6e454f3620c4d6aca17ad072d3f8bbb2760eac3b2ad391b5e8073,
v1=130dcacb53a94d983a37cf2acba98e805a1c37185309ba56fdcccbcf00d6dd8b
(Note that the actual header value is sent as a single string without any new lines.)

Verifying the Signature

A psuedo-algorithm to verify the signature of a webhook delivery is described below.

Step 1: Extract the signature(s) from the request

Extract the signature string from the X-Zeplo-Signature header on the request. Split the signatures which are separated by the , character. Select only signatures which are version v1 and remove the v1= prefix.

Step 2: Compute the expected valid signature

Using the received JSON payload (entire request body):

Compute the SHA-256 HMAC using the shared secret as the key. Take the Base16 encoding of the result to obtain a value we can compare.

Step 3: Compare the signatures

Compare the expected signature obtained in Step 2 with the provided signatures from Step 1.

If at least one of the signatures matches, the webhook should be considered a trusted and authentic request from Zeplo that can be processed by the server.

Note: When comparing signatures, be sure to use a constant-time string comparison to protect against timing attacks.

Example

const crypto = require('crypto')

function verifySignature (secret: string, body: string, signatures: string, version = 'v1') {
  const signature = crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex')

  const signatureWithVersion = version + "=" + signature
  const signatureList =  signatures.split(",")

  return signatureList.indexOf(signatureWithVersion) > -1
}

Webhook Body

The body of the webhook sent will have the following format:

{
  "event": {
    "id": "112b4b19-9fac-4899-a176-1aa1c41c5172",
    "event": "request.create",
  },
  "webhook": "69f2231b-f062-4ec0-870c-fee7b7b73ff4",
  "data": {
    "type": "request",
    "object": {
      "id": "49b5fa70-32bb-493f-9f53-140da97a0a85-iow",
      "workspace": "zeplo",
      "key": "default",
      "trace": "713e3428-40c5-4b29-cbe5-e0d591de830a-iow",
      "status": "SUCCESS",
      "source": "SCHEDULE",
      "start": 1610986500,
      "end": 1610986500.407,
      "duration": 0.407,
      "env": "production",
      "request": {
        "method": "POST",
        "url": "https://api.zeplo.io/sources/sync?x=1",
        "headers": {
          "accept": "application/json, text/plain, */*",
          "user-agent": "axios/0.19.2",
          "content-type": "application/json;charset=utf-8",
          "content-length": "51",
          "x-cloud-trace-context": "745a56f71b3ea481a923fb322a75628d/171973945206328;o=1"
        },
        "scheme": "https",
        "host": "api.zeplo.io",
        "path": "/sources/sync",
        "params": {
          "x": "1"
        },
        "hasbody": true,
        "start": 1610841409.119
      },
      "response": {
        "status": 204,
        "statustext": "No Content",
        "headers": {
          "date": "Mon, 18 Jan 2021 16:15:00 GMT",
          "etag": "W/\"a-bAsFyilMr4Ra1hIU5PyoyFRunpI\"",
          "server": "Google Frontend",
          "connection": "close",
          "content-type": "text/html",
          "content-length": "0",
          "x-cloud-trace-context": "745a56f71b3ea481a923fb322a75628d/12639389939611967624;o=1, 745a56f71b3ea481a923fb322a75628d;o=1",
          "access-control-allow-origin": "*"
        },
        "hasbody": true,
        "body": { "x": 1 } // only JSON or string values will appear
      },
      "attempts": 1,
      "timezone": "UTC",
      "received": 1610985600.018
    }
  }
}