Webhooks are available on our paid plans. Please contact us if you are interested in this feature.

What does Nami provide via Webhook?

Nami provides a variety of data generated by use of our platform that can be sent via webhook to your servers or a third-party platform.

See our list of webhooks event types for everything Nami supports.

The Webhook Payload

The webhook payload will always have the following fields:

  • created_date ISO 8601 format time field for when the event was created
  • event_type the hierarchial event type
  • id unique id for the event
  • user_id Nami's unique user ID associated with the event

Additional data will be available based on the event_type. Take a look at the event types guide for more details.

A full JSON schema for all event payloads is also available here.

Sending of the Webhook

Nami sends events as a UTF-8 encoded JSON body with a header nami-signature that allows for validation.

Nami expects your application will return a 2xx status code upon a successful webhook receipt. If we receive any other status code, we will continue to retry sending the message. The retry logic has exponential backoff and some randomness. On average it will continue to retry once an hour for up to 24-hours before it stops trying to send a particular payload.


Nami Best Practice

Because of the retry logic and the asynchronous nature of the sending of the webhooks there is no guarantee that you will receive each event in time order.

We recommend that you check the time stamp on the data you are receiving. In particular for user.subscription events, if you've received a more recent event, you can ignore the old one if all you need to know is your user's current subscription status.

Setting up the Webhook

  1. Navigate to Integrations and click on Webhook.
  1. Give your webhook a name.
  1. Select an option for the User Login Event Filter.
  • Select 'Wait for user to login before delivering' if your app has accounts with external identifiers and you are using webhooks to update CRM state for logged in users. With this option, Nami will wait until an external id is set before delivering the webhook, ensuring that the webhook data always matches your CRM. Nami will continue retrying the webhook at minimum once a day until the user signs in.
  • Select 'Deliver all events immediately' if your app doesn't have accounts or if you want a record of all users regardless of signed in state.
  1. Fill out the URL to use for your webhook endpoint.


Webhook URLs must be HTTPS

  1. Select events you would like your webhook to subscribe to. See our list of webhooks event types for details.
  1. Select the switch to Enable Integration and Add New Integration. This creates and turns the integration on so that Nami starts sending data to your webhook.
  1. Click the Webhook card under 'My Integrations' to view your newly created integration.
  1. Reveal and Copy the "Signing Secret". Use this value to validate the data. See below for information on validation and security. You can also use the "Rotate" button to periodically create a new signing secret.


Use Caution When Rotating the Signing Secret

If you are validating the nami signature on the receiving end of a webhook, be aware that rotating the Signing Secret may break your webhook integration.

Be sure to update the Signing Secret on the receiving end of your webhook immediately after rotating.

Validation and Security

We provide a couple of options that you may use to both secure your webhook endpoint and validate that the data you are receiving is coming from Nami.

  1. Nami hashes the payload of all data sent with HMAC-SHA-256 and a shared secret that is available in the webhook setup page. The result of this hash is added as nami-signature to the header of the request. You can validate that the signature is correct on your end after receiving the data.

The following code sample can be used to receive a webhook, validate the signature, and respond to the webhook as either successful or failed.

from flask import Flask, request
import hashlib
import hmac
import os

NAMI_SIGNING_SECRET = bytes(os.environ["NAMI_SIGNING_SECRET"].encode("utf-8"))

app = Flask(__name__)

def compute_signature(data: bytes, secret: bytes) -> str:
  return hmac.new(

@app.route("/webhook", methods=["POST"])
def webhook():
  unverified_event = request.data
  received_nami_signature = request.headers.get("nami-signature")
  expected_signature = compute_signature(unverified_event, NAMI_SIGNING_SECRET)

  if not received_nami_signature:
    return ({"error": "nami-signature header not received."}, 400)

  if hmac.compare_digest(expected_signature, received_nami_signature):
    # Do Webhook Processing.
    validated_event = request.json

    # Notify server the request succeeded
    return ("", 204)
    # Webhook fails validation, do not process the event.
    return (
        "error": "Failed Signature Validation",
        "received": received_nami_signature,
        "expected": expected_signature,
const express = require('express')
const bodyParser = require('body-parser')
const crypto = require('crypto')

const PORT = 3000

const app = express()

  verify: (req, _, buf) => {
    req.rawBody = buf.toString()

app.post('/webhook', (req, res) => {
  const eventText = req.rawBody
  const event = req.body
  const receivedSignature = req.header('Nami-Signature')
  const expectedSignature = computeSignature(eventText, NAMI_SIGNING_SECRET)

  if (!receivedSignature) {
    return res.status(400).send({"error": "Nami-Signature header not received."})

  if (expectedSignature === receivedSignature) {
    // Do Webhook Processing.
    console.log('Process ', event)
    // Notify server the request succeeded
    return res.status(204).send()

  // Webhook fails validation, do not process the event.
  return res.status(400).send({
    error: "Failed Signature Validation",
    received: receivedSignature,
    expected: expectedSignature,

app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`)

 * @param data {string}
 * @param secret {string}
 * @returns {string}
function computeSignature(data, secret) {
  return crypto.createHmac('sha256', secret).update(data).digest('hex')
public ProcessNotificationPayloadAsync([FromBody] NamiSubscriptionPayLoad namiNotification)


if (Request.Headers.TryGetValue("Nami-Signature", out var headerValue))


var eventText = await GetRawBodyAsync(Request);

var expectedSignature = GetHash(eventText, _appSettings.NamiHmacAuthKey);

var isExpected = expectedSignature == headerValue;



public static String GetHash(String text, String key)


ASCIIEncoding encoding = new UTF8Encoding();

Byte[] textBytes = encoding.GetBytes(text);

Byte[] keyBytes = encoding.GetBytes(key);

Byte[] hashBytes;

using (HMACSHA256 hash = new HMACSHA256(keyBytes))

hashBytes = hash.ComputeHash(textBytes);

return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();

  1. Nami webhooks come from a fixed IP address so you may explicitly allow traffic from our servers. You can retrieve the current list of IPs to add to your allow list from this endpoint and then look at the array for the key "webhook_outbound_ips".


Nami Best Practice

We recommend that you poll this endpoint on a regular basis and update your system with the latest list of outbound IPs for our webhooks. Once a day should be sufficient.