Webhooks
Webhooks are an important part of your integration. When building Velstack integrations, you might want Velstack to notify your applications about events such as a successful SMS delivery or a failed message as they occur in your accounts, so that your backend systems can execute actions accordingly.
A webhook URL is an endpoint on your server where you can receive notifications about such events.
When an event occurs, we'll make a POST request to that endpoint, with a JSON body containing the details about the event,
including the type of event and the data associated with it.
Webhooks use cases
We recommend using webhooks to provide updates to your customers instead of relying on callbacks or polling. With callbacks, neither we nor you have control over what happens on the customer's end. Callbacks can fail if the customer's device experiences a network connection issue or goes offline during message delivery.
In a nutshell, a bulk SMS campaign can be sent successfully but when there are delivery issues, callbacks can fail and you might not get notified about delivery status.
- When SMS delivery status is updated
- A pending/scheduled SMS transitions to sent or failed
These are all asynchronous actions, they are not controlled by your application, so you won't know when they are completed, unless we notify you or you check later.
Creating webhook URL
To register a new webhook, you need to setup a URL on your dashboard that Velstack can call. You can configure a new webhook from the Velstack dashboard under API settings . During testing, you can setup an instant URL by visiting webhook.site. This will allow you to inspect the received payload without writing any code or set up a server.
Here is how to setup and configure webhook url in your application.
- The endpoint you specify on your dashboard must be the same as the endpoint in your application.
- If the webhook endpoint in your application is using CSRF protection, please disable it.
Creating endpoint
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">express</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">require</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'express'</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">app</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">express</span><span style="color: var(--shiki-color-text)">();</span></span> <span><span style="color: var(--shiki-token-constant)">app</span><span style="color: var(--shiki-token-function)">.use</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">express</span><span style="color: var(--shiki-token-function)">.json</span><span style="color: var(--shiki-color-text)">());</span></span> <span></span> <span><span style="color: var(--shiki-token-constant)">app</span><span style="color: var(--shiki-token-function)">.post</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'/webhook'</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> (req</span><span style="color: var(--shiki-token-punctuation)">,</span><span style="color: var(--shiki-color-text)"> res) </span><span style="color: var(--shiki-token-keyword)">=></span><span style="color: var(--shiki-color-text)"> {</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// Process the webhook payload</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-comment)">// ...</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">res</span><span style="color: var(--shiki-token-function)">.status</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">200</span><span style="color: var(--shiki-color-text)">)</span><span style="color: var(--shiki-token-function)">.json</span><span style="color: var(--shiki-color-text)">({ message</span><span style="color: var(--shiki-token-keyword)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">'Webhook received'</span><span style="color: var(--shiki-color-text)"> });</span></span> <span><span style="color: var(--shiki-color-text)">});</span></span> <span></span>
Now, whenever any event happens in your on account, a webhook is fired off by Velstack to this endpoint.
If your app doesn't respond with a response code starting with 2XX, Velstack will assume that the event failed. So we will retry sending the webhook after 1 minute. If that second attempt fails, Velstack will attempt to call the webhook a final time after 5 minutes.
Listening to Webhooks
-
When your app receives a webhook request from Velstack, check the
eventattribute to see what event was sent. Theeventwill tell you the payload type, e.g.,sms_sent,sms_failed, etc. -
Since your webhook URL is publicly accessible, you need to verify that events originate from Velstack and not from a malicious server. This ensures that the event payload has not been tampered with during transmission from Velstack's server.
Validate Signature
Events sent from Velstack include the X-VELSTACK-SIGNATURE in the request header.
This header's value is an HMAC SHA256 hash of the event payload, signed using your SECRET_KEY.
You should verify the header signature before making any database queries or processing the event.
Validate signature
<span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">express</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">require</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'express'</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">crypto</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">require</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-string-expression)">'crypto'</span><span style="color: var(--shiki-color-text)">);</span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">app</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">express</span><span style="color: var(--shiki-color-text)">();</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">secret</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"sk_live_rcKGRa3Tagsp6pygEA7bY9oYUEGgYBVqFI8y5nDbc0aa0d7b"</span><span style="color: var(--shiki-color-text)">;</span></span> <span></span> <span><span style="color: var(--shiki-token-constant)">app</span><span style="color: var(--shiki-token-function)">.use</span><span style="color: var(--shiki-color-text)">(</span><span style="color: var(--shiki-token-constant)">express</span><span style="color: var(--shiki-token-function)">.json</span><span style="color: var(--shiki-color-text)">());</span></span> <span></span> <span><span style="color: var(--shiki-token-keyword)">const</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-function)">isValidVelstackCallbackSignature</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">=</span><span style="color: var(--shiki-color-text)"> (req) </span><span style="color: var(--shiki-token-keyword)">=></span><span style="color: var(--shiki-color-text)"> {</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">```python</span></span> <span><span style="color: var(--shiki-token-string-expression)">from flask import Flask, request, jsonify</span></span> <span><span style="color: var(--shiki-token-string-expression)">import hmac</span></span> <span><span style="color: var(--shiki-token-string-expression)">import hashlib</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">app = Flask(__name__)</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">SECRET = "sk_live_rcKGRa3Tagsp6pygEA7bY9oYUEGgYBVqFI8y5nDbc0aa0d7b"</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">def is_valid_velstack_callback_signature(payload, signature):</span></span> <span><span style="color: var(--shiki-token-string-expression)"> calculated_signature = hmac.new(</span></span> <span><span style="color: var(--shiki-token-string-expression)"> SECRET.encode('utf-8'), payload.encode('utf-8'), hashlib.sha256</span></span> <span><span style="color: var(--shiki-token-string-expression)"> ).hexdigest()</span></span> <span><span style="color: var(--shiki-token-string-expression)"> return hmac.compare_digest(calculated_signature, signature)</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">@app.route('/webhook', methods=['POST'])</span></span> <span><span style="color: var(--shiki-token-string-expression)">def handle_webhook():</span></span> <span><span style="color: var(--shiki-token-string-expression)"> payload = request.get_data(as_text=True)</span></span> <span><span style="color: var(--shiki-token-string-expression)"> signature = request.headers.get('X-VELSTACK-SIGNATURE')</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)"> if is_valid_velstack_callback_signature(payload, signature):</span></span> <span><span style="color: var(--shiki-token-string-expression)"> app.logger.info('is valid now ' + payload)</span></span> <span><span style="color: var(--shiki-token-string-expression)"> return jsonify({}), 200</span></span> <span><span style="color: var(--shiki-token-string-expression)"> else:</span></span> <span><span style="color: var(--shiki-token-string-expression)"> return 'Invalid signature', 400</span></span> <span></span> <span><span style="color: var(--shiki-token-string-expression)">if __name__ == '__main__':</span></span> <span><span style="color: var(--shiki-token-string-expression)"> app.run(port=5000)</span></span> <span></span> <span></span>
If your generated signature matches the x-velstack-signature header,
you can be sure that the request was truly coming from Velstack.
Supported events
<span><span style="color: var(--shiki-color-text)">{</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"event"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"sms.sent"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"data"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> {</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"id"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">17207978934486</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"status"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"sent"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"reference"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"s7KKcsxXr6j4NSbE"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"phone"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"+233501234567"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"message"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"Your OTP is 1234"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"sms_count"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-constant)">1</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"message_type"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"transactional"</span><span style="color: var(--shiki-token-punctuation)">,</span></span> <span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-keyword)">"created_at"</span><span style="color: var(--shiki-token-punctuation)">:</span><span style="color: var(--shiki-color-text)"> </span><span style="color: var(--shiki-token-string-expression)">"2024-07-12T15:24:53.000000Z"</span></span> <span><span style="color: var(--shiki-color-text)"> }</span></span> <span><span style="color: var(--shiki-color-text)">}</span></span> <span></span>
Event types
- Name
sms.sent- Type
- Description
A message was successfully sent to the recipient
- Name
sms.failed- Type
- Description
A message failed to send