# 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 |
|---|