Nooks Webhooks Integration Guide

Last updated: April 24, 2026

Learn how to configure the Nooks call logging Webhooks for your Workspace.

Overview

Nooks call logging Webhooks provide a way to stream your call data to external systems in real-time. The primary goal of this feature is to empower you to use your Nooks call data in your own custom workflows.

By subscribing to the call.logged event, you can:

  • Integrate with CRMs or Sales Engagement Platforms (SEPs) that Nooks does not natively support.

  • Stream call logs directly into your own data lakes or data warehouses for analysis.

  • Trigger custom internal workflows, such as sending notifications to Slack, updating dashboards, or enrolling prospects in new sequences.

This guide will walk you through the technical steps to securely receive, verify, and process these events.

Adding Call Logging Webhooks

Webhooks can be added from the Webhooks setting page under the Integrations section. This is what it should look like after successful saving of a webhook:

attachment-5c97a180-e86a-4199-92bc-1cdb4b45896e-Screenshot_2026-01-27_at_10.46.16_AM.png.webp

Activating Your Endpoint

When you save a new endpoint URL in your Nooks settings, our system will immediately perform a one-time "ping" to ensure the URL is valid and can receive requests. If the endpoint cannot be pinged then save operation will fail.

Critical: After the endpoint is saved successfully, you will get a signing key in the format of nooks-webhook-signing-key-xyz. Save it in a secure location right away since the signing key is only shown once.

The only requirement for your endpoint to be activated is that it must respond to this initial POST request with an HTTP 2xx status code (e.g., 200 OK or 202 Accepted). If we receive a 2xx response, your endpoint will be marked "active" and we will begin sending call.logged events after you verify the Webhook URL from the settings page. For security reasons, it is highly recommended to verify before setting up the x-webhook-signature verification (Next Section).

[Highly Recommended] Verifying the x-webhook-signature

Every event we send after activation includes an x-webhook-signature header. This is the security feature (recommended) of our call logging webhook system.

Verifying this signature is essential and allows you to confirm two things:

  1. Authenticity: The request genuinely came from Nooks.

  2. Integrity: The payload (the data) was not altered in transit.

The header has this format: t=<timestamp>,s=<signature>.

  • t: The Unix millisecond timestamp of the request.

  • s: The HMAC-SHA256 signature, encoded in base64.

The signature (s) is generated on our end using your unique Signing Key (which is provided once you successfully save the webhook URL) and this formula: s = HMAC-SHA256(timestamp + "." + raw_body, signingKey).

Note: The raw_body is the literal, unparsed string of the request body.

Verification Steps

To verify the signature, your endpoint must perform the following steps on every incoming request:

  1. Get Your Secret: After retrieving your unique Signing Key from your Nooks settings, keep it in a secure place (like GCP Secret Manager).

    1. NOTE: const signing_key = 'nooks-webhook-signing-key-xyz' at the top of the script is not secure.

  2. Parse the Header: Read the x-webhook-signature header. Split it by , to separate the t (timestamp) and s (signature) parts.

    1. Compare: Use a timing-safe comparison method (like crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python) to check if your computed signature matches the s from the header. If they match, the request is authentic.

  3. Check the Timestamp: This is a crucial step to prevent "replay attacks." Convert t to a number and compare it to the current time. We require that you reject any request older than 5 minutes.

  4. Build the Signed String: You must re-create the exact string we signed. Concatenate the timestamp (t), a literal period (.), and the raw body of the request (e.g., dataToSign = timestamp + "." + raw_body).

  5. Compute Your Signature: Using your secret Signing Key, compute an HMAC-SHA256 hash of the string you just built in Step 4. Make sure to base64 encode the result.

Code Examples

import crypto from 'crypto';

// Get this from your Nooks settings
const NOOKS_SIGNING_KEY = 'nooks_sk_your_secret_key_here';
const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000; // 5 minutes

function verifyNooksSignature(rawBody: string, signatureHeader: string): boolean {
  try {
    const parts = {
      t: '',
      s: '',
    };
    for (const part of signatureHeader.split(',')) {
      const [key, value] = part.split('=');
      if (key === 't' || key === 's') {
        parts[key] = value;
      }
    }

    if (!parts.t || !parts.s) {
      throw new Error('Invalid signature header format');
    }

    // 1. Check the timestamp
    const timestamp = parseInt(parts.t, 10);
    if (Date.now() - timestamp > MAX_TIMESTAMP_AGE_MS) {
      console.error('Timestamp check failed. Possible replay attack.');
      return false;
    }

    // 2. Build the string to sign
    const dataToSign = `${timestamp}.${rawBody}`;

    // 3. Compute your signature
    const expectedSignature = crypto
      .createHmac('sha256', NOOKS_SIGNING_KEY)
      .update(dataToSign)
      .digest('base64');

    // 4. Compare signatures
    const sigBuffer = Buffer.from(parts.s, 'base64');
    const expectedSigBuffer = Buffer.from(expectedSignature, 'base64');

    return crypto.timingSafeEqual(sigBuffer, expectedSigBuffer);

  } catch (error) {
    console.error('Signature verification failed:', error);
    return false;
  }
}
import time
import hmac
import hashlib
import base64

# Get this from your Nooks settings
NOOKS_SIGNING_KEY = "nooks_sk_your_secret_key_here"
MAX_TIMESTAMP_AGE_S = 5 * 60  # 5 minutes

def verify_nooks_signature(raw_body: bytes, signature_header: str) -> bool:
    try:
        parts = {part.split('=')[0]: part.split('=')[1] for part in signature_header.split(',')}
        timestamp_str = parts.get('t')
        signature = parts.get('s')

        if not timestamp_str or not signature:
            raise ValueError("Invalid signature header format")

        # 1. Check the timestamp
        timestamp = int(timestamp_str)
        if (time.time() * 1000) - timestamp > (MAX_TIMESTAMP_AGE_S * 1000):
            print("Timestamp check failed. Possible replay attack.")
            return False

        # 2. Build the string (raw_body must be bytes)
        data_to_sign = f"{timestamp}.".encode('utf-8') + raw_body

        # 3. Compute your signature
        expected_signature_hash = hmac.new(
            NOOKS_SIGNING_KEY.encode('utf-8'),
            data_to_sign,
            hashlib.sha256
        )
        
        # 4. Compare signatures
        return hmac.compare_digest(
            base64.b64decode(signature),
            expected_signature_hash.digest()
        )
        
    except Exception as e:
        print(f"Signature verification failed: {e}")
        return False

Requirements for Responding to Webhooks

To ensure our system can reliably deliver events to you, your endpoint must follow two critical rules for every event it receives.

  1. Respond with a 2xx Status Code: You must always send an HTTP 2xx status code (e.g., 200, 202, or 204) to acknowledge that you have successfully received the event.

  2. Respond Quickly: Your endpoint must send its response within 15 seconds. Any response that is not a 2xx or that takes longer than 15 seconds will be considered a failure, and we will attempt to retry the delivery.

[Highly Recommended] Process Asynchronously

To comply with the 15-second timeout, you should not perform any complex or time-consuming tasks (like calling other APIs or writing to a database) before sending your response. We highly recommend you use a queue or an asynchronous function to handle the processing after you have acknowledged the request.

  • Do this:

    1. Receive the request.

    2. Verify the x-webhook-signature. If it fails, respond with 401 Unauthorized.

    3. Add the event to an internal queue (like RabbitMQ, SQS, or BullMQ).

    4. Immediately send your 200 OK response.

    5. Process the event from your queue.

  • Don’t do this

    1. Receive the request.

    2. Verify the x-webhook-signature. If it fails, respond with 401 Unauthorized.

    3. Connect to your data warehouse.

    4. Write the data.

    5. Call your CRM's API to update a record.

    6. (16 seconds later) Send a 200 OK response. ← This will fail.

Retries & Idempotency

Retries (Nooks' Responsibility)

If your endpoint fails to respond with a 2xx status code within the 15-second timeout, we will consider the delivery a failure.

To ensure you receive the data, we will automatically retry sending the event. Our system uses an exponential backoff strategy with jitter, for up to 8 attempts over 30 minutes.

Idempotency (Your Responsibility)

Because of our retry system, it's possible for your endpoint to receive the same call data more than once. To prevent creating duplicate records in your system, you must make your processing idempotent.

Do NOT use eventId for de-duplication. The eventId is unique for each delivery attempt (i.e., it will be different for each retry) and is only for logging.

To safely de-duplicate, you must use the callId located inside the callData object. This ID is permanent for the call and will be identical across all retry attempts for that event.

{
  "event": "call.logged",
  "eventId": "uuid-for-this-specific-attempt-A",
  "callData": {
    "callId": "call_12345-THIS-IS-YOUR-KEY", // <-- USE THIS ID FOR IDEMPOTENCY
    ...
  }
}

Recommended Logic: When you receive an event (after verifying its signature):

  • Check your database/cache to see if this callData.callId has already been successfully processed.

  • If yes, you have already handled this. Respond 200 OK and stop.

  • If no, add the event to your processing queue.

  • Respond 200 OK immediately.

Payload Structure: call.logged

The call.logged event fires once a call is fully finalized in Nooks. All events are sent via HTTP POST with a Content-Type: application/json header.

The payload contains all the metadata associated with the call, nested within the callData object.

Top-Level Fields

  • event (String): The name of the event. For this webhook, the value is always call.logged.

  • eventId (String): A unique string generated for this specific delivery attempt. Use this for logging and troubleshooting. Do not use this for idempotency.

  • occurredAt (String): An ISO 8601 timestamp for when the event was finalized.

    • Example: 2025-11-12T00:29:58.640Z

  • callData (Object): The object containing all metadata about the call itself. This is where you will find the callId for idempotency.

callData Field Details

This object contains the complete details of the logged call.

  • status (String): The final status of the call.

    • Example: "completed"

  • callId (String): The unique and permanent identifier for this call. You can use this field for idempotency (de-duplication).

  • workspaceId (String): The Nooks identifier for your organization's workspace.

  • userData (Object): An object containing information about the Nooks user who made the call.

    • userId (String): The user's unique ID.

    • email (String): The user's email address.

    • name (String): The user's full name.

  • prospectData (Object): An object containing information about the prospect (contact or lead) who was on the call.

    • prospectId (String): The prospect's SEP or CRM ID.

    • name (String): The prospect's full name.

    • phoneNumber (String): The phone number that was dialed.

    • email (String | null): The prospect's primary email address, if available.

    • linkedInUrl (String | null): The prospect's LinkedIn profile URL, if available.

  • accountData (Object): An object containing information about the account associated with the prospect.

    • accountId (String): The account's CRM ID.

    • name (String): The account's name.

  • callDirection (String): Indicates whether the call was inbound or outbound.

  • disposition (Object): An object containing the call disposition set by the user.

    • id (String): The unique ID for the disposition.

    • name (String): The human-readable name of the disposition (e.g., "No Answer", "Connected").

  • startedAt (String): An ISO 8601 timestamp for when the call began.

  • durationSeconds (Number): The total duration of the call in seconds, as a floating-point number.

  • recordingUrl (String | null): Recording URL of the call, if a recording exists.

  • notes (String | null): Call notes entered by the rep after the call, if any.

  • transcriptUrl (String | null): A direct link to the call transcript page in Nooks.

  • sequenceData (Object | null): Attribution data for the sequence (campaign) that triggered this call, if the call was made from a sequence.

    • sequenceName (String | null): The name of the sequence the call was made from.

    • sequenceStep (String | null): The step within the sequence that triggered this call.

Here is an example Payload:

{
  "event": "call.logged",
  "eventId": "ed573041-57e9-4ef6-9976-bb759761c2a7-#-22216d73-1351-4e4b-ae0c-64d7fc46b8c1",
  "occurredAt": "2025-12-09T04:06:36.145Z",
  "callData": {
    "callId": "ed573041-57e9-4ef6-9976-bb759761c2a7",
    "workspaceId": "cUvwa9fcNtE1G7vy",
    "status": "completed",
    "userData": {
      "userId": "test-user-id",
      "email": "test-user-email",
      "name": "test-user-name"
    },
    "prospectData": {
      "prospectId": "0056s00000D1gRtAAJ",
      "name": "Jane Smith",
      "phoneNumber": "+14155551234",
      "email": "jane.smith@acme.com",
      "linkedInUrl": "https://www.linkedin.com/in/janesmith"
    },
    "accountData": {
      "accountId": "test-account-id",
      "name": "test-account-name"
    },
    "callDirection": "inbound",
    "disposition": {
      "id": "test-disposition-id",
      "name": "test-disposition-name"
    },
    "startedAt": "2025-12-09T02:54:29.170Z",
    "durationSeconds": 100,
    "recordingUrl": "https://storage.googleapis.com/recording-url-example",
    "notes": "Spoke with Jane about renewal. She wants a follow-up demo next week.",
    "transcriptUrl": "https://app.nooks.in/workspaces/cUvwa9fcNtE1G7vy/transcript?callId=ed573041-57e9-4ef6-9976-bb759761c2a7",
    "sequenceData": {
      "sequenceName": "Q1 Enterprise Outbound",
      "sequenceStep": "Call Step 2"
    }
  }
}

Troubleshooting

This section covers the most common errors and failure points.

Failed to Save Webhook URL

If you encounter a 4xx or 5xx error when attempting to save a webhook URL, it is typically due to authorization issues on your endpoint. During the save process, Nooks sends an initial test payload to verify the URL's validity and accessibility. If this request is rejected or fails to reach your server, the save operation will fail. Please ensure your endpoint is configured to grant the Nooks application the necessary permissions to receive and respond to this test payload.

For a complete list of HTTP response codes and their technical definitions, you can refer to the MDN Web Docs: HTTP Response Status Codes.

Error: "Verification failed: Invalid signature"

This is the most common error and means your computed signature did not match the one we sent.

Check these 99% of the time:

  1. Check Your Signing Key: Ensure you have copied the correct signingKey from your Nooks settings, with no extra spaces or characters.

  2. Use the Raw Request Body: You must use the literal, unparsed string of the request body. Do not parse the JSON and then re-stringify it. The rawBody string must be byte-for-byte identical to what we sent.

  3. Verify Your Formula: The string you sign must be exactly timestamp + "." + rawBody. A common mistake is using timestamp + rawBody (missing the period).

Error: "Timestamp check failed" or "Timestamp is too old"

This error means your server rejected the request because the timestamp (t) in the header was older than your 5-minute window.

  1. Check Your Server's Clock: This is the most common cause. Ensure your server's clock is accurately synced with an NTP (Network Time Protocol) service. A significant clock drift will cause you to fail valid, on-time requests.

  2. Check for Slow Processing: If your endpoint is slow to respond, it's possible a request sits in a queue before you process it, causing it to become "stale." Your verification must happen immediately upon receiving the request.

Nooks Logs Show 4xx or 5xx Failures

If our logs show that your endpoint is responding with a 4xx or 5xx error, or is timing out:

  1. Check Your 15-Second Timeout: Our system will fail any request that does not receive a response within 15 seconds. This is why you must process events asynchronously. (See Section 3).

  2. Check Your Response Code: You must respond with an HTTP 2xx code. If your server crashes and responds 500 Internal Server Error, or 401 Unauthorized (due to a bad signature check), we will register it as a failure and begin retrying.

Helpful Resources

The patterns used in this guide, such as handshakes and signed signatures, are industry best practices for building secure and reliable webhook systems. If you'd like to learn more about the concepts behind this design, here are a few excellent resources.