# Webhooks

Webhooks allow you to receive real-time updates when a specific event occurs, e.g. when a message is received from a contact. These events are HTTP POST requests sent to an endpoint on your server. The request body is in JSON format. All events are delivered to a single webhook URL (e.g. https://your-server.com/webhook).

# Adding an endpoint

To start receiving webhooks, you will need to create a new endpoint on the Webhooks page (opens new window) in the Smartsupp dashboard.

Fill in your server's URL and pick the events you are interested in receiving.

Done, you are now subscribed to webhook delivery.

# Request headers

Every webhook request carries the following headers:

Header Description
webhook-id Unique identifier of the message. Stays the same across retries of the same delivery - use it for deduplication.
webhook-timestamp Unix timestamp (seconds) of the delivery attempt.
webhook-signature Signature of the request, see Verification.
X-Smartsupp-Hmac Deprecated. Legacy signature, see Legacy verification. It will stop being sent in the future. Switch to webhook-signature.

Older integrations may also receive the X-Smartsupp-App-Id header identifying which integration sent the webhook. New subscribers won't receive this header.

# Responding to webhooks

When you receive a webhook event, respond with a 2xx status code (e.g. 200 OK) as quickly as possible. Do any time-consuming processing asynchronously, after the response is sent.

When your endpoint responds with a non-2xx status code or times out, the delivery is retried automatically with an exponential backoff schedule. An endpoint that keeps failing for an extended period of time may be automatically disabled - make sure failures on your side are noticed and fixed.

# Verification

# Endpoint secret

You can obtain the webhook secret in the detail of the particular endpoint on the Webhooks page (opens new window) in the Smartsupp dashboard.

# Verifying the signature

Webhook signatures follow the Standard Webhooks (opens new window) specification.

The webhook-signature header contains a base64-encoded HMAC-SHA256 signature of the string {webhook-id}.{webhook-timestamp}.{raw request body}, prefixed with the scheme version (v1,). The header may contain multiple space-separated signatures (e.g. during a secret rotation) - the request is valid if any of them matches.

The signing key is your endpoint secret (see Endpoint secret). If you verify with a Standard Webhooks compatible library, provide the secret in its expected format - whsec_ followed by the base64 encoding of your endpoint secret:

const signingSecret = 'whsec_' + Buffer.from('YOUR_ENDPOINT_SECRET', 'utf8').toString('base64')

Raw body required

Signatures are computed over the exact raw bytes of the request body. Verify against the unparsed body. A re-serialized JSON object may differ byte-by-byte and fail verification.

The following code can be used to verify a webhook request in Node.js (with koa):

import {createHmac, timingSafeEqual} from 'crypto'
import koaBody from 'koa-body'

const UNPARSED_BODY = Symbol.for('unparsedBody')
const secret = 'YOUR_ENDPOINT_SECRET'

function verifySignature(secret, headers, rawBody) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signature = headers['webhook-signature']
  if (!id || !timestamp || !signature) {
    return false
  }

  const expected = createHmac('sha256', secret)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest()

  return signature.split(' ').some((entry) => {
    const [version, sig] = entry.split(',')
    if (version !== 'v1' || !sig) {
      return false
    }
    const candidate = Buffer.from(sig, 'base64')
    return candidate.length === expected.length && timingSafeEqual(candidate, expected)
  })
}

router.use(koaBody({
  includeUnparsed: true, // required to return raw body as string
}))
router.use((ctx, next) => {
  if (!verifySignature(secret, ctx.headers, ctx.request.body[UNPARSED_BODY])) {
    ctx.throw(403, 'Invalid webhook signature')
  }
  return next()
})

Optionally, you can also reject deliveries whose webhook-timestamp is too far in the past (e.g. more than 5 minutes) to protect against replay attacks.

# Legacy verification (deprecated)

Deprecated

The X-Smartsupp-Hmac header is deprecated and will stop being sent in the future.

Each webhook request includes an X-Smartsupp-Hmac header containing a hex-encoded HMAC-SHA256 signature of the raw request body, generated with the same endpoint secret:

import {createHmac} from 'crypto'
import koaBody from 'koa-body'

const UNPARSED_BODY = Symbol.for('unparsedBody')
const secret = 'YOUR_ENDPOINT_SECRET'

function generateHmac(secret, body) {
  return createHmac('sha256', secret).update(body).digest('hex')
}

router.use(koaBody({
  includeUnparsed: true, // required to return raw body as string
}))
router.use((ctx, next) => {
  if (ctx.headers['x-smartsupp-hmac'] !== generateHmac(secret, ctx.request.body[UNPARSED_BODY])) {
    ctx.throw(403, 'Invalid request hmac signature')
  }
  return next()
})

Note that unlike the webhook-signature scheme, the legacy signature covers only the request body, so it does not protect against replay attacks.

# Structure

All webhooks have the following body structure:

{
  "type": "event_callback",
  "event": "<name>",
  "timestamp": "2020-08-08T07:53:18.825Z",
  "account_id": "881987",
  "data": {
    ...
  }
}
Property Type Description
type string Always has the value "event_callback".
event string Name of the event (e.g. "conversation.closed").
timestamp string Date when the event was created, in ISO format.
account_id string Identifies the account.
data object Event-related data.

# Events

Name Description