Skip to content
Last updated

Webhooks & Real-Time Events

Webhooks push events to your server in real time as things happen -- orders placed, statuses changed, inventory updated. Instead of polling the API repeatedly, you register an HTTPS endpoint and Tote delivers events to it as they occur.

This guide covers the complete webhook integration: subscribing to events, verifying signatures, handling retries, processing events idempotently, and falling back to polling when needed.

Prerequisites

Before following this guide, make sure you have:

  • Completed the Getting Started guide -- You have a Tote developer account and can obtain access tokens.
  • An access token -- See the Authentication guide.
  • An HTTPS endpoint -- Your server must expose a publicly reachable HTTPS URL to receive webhook events. HTTP URLs are rejected.

All examples use the sandbox base URL and consistent IDs from prior guides:

Base URL:    https://sandbox.api.tote.ai/v1/online-ordering
Location ID: b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d
Order ID:    f9a8b7c6-d5e4-3210-fedc-ba9876543210

Event Types

Tote delivers six event types covering orders, inventory, menu, and location changes.

Event TypeFires WhenUse Case
order.createdNew order placed via checkoutStart order preparation workflow
order.status_changedAny of the 3 status fields changesUpdate order tracking UI
order.cancelledOrder is cancelledStop preparation, process refunds
stock.updatedItem availability changes at a locationUpdate menu availability display
menu.changedMenu items or categories modifiedRe-sync local menu cache
location.hours_changedBusiness hours updatedUpdate store hours display

Event envelope

All events share the same envelope structure. The data field contains the event-specific payload.

{
  "event_id": "evt_c3d4e5f6-a7b8-9012-cdef-234567890123",
  "event_type": "order.created",
  "created_at": "2026-02-01T14:00:00Z",
  "data": { ... }
}
FieldTypeDescription
event_idUUIDUnique identifier for this event. Use for idempotent processing.
event_typestringDetermines the shape of the data field. One of the six types listed above.
created_atISO 8601When the event occurred (UTC).
dataobjectEvent-specific payload. Shape depends on event_type.

Managing Subscriptions

Webhook subscriptions are managed via REST endpoints. You create a subscription to register your endpoint, list subscriptions to check their status, and delete them when no longer needed.

Creating a subscription

Register your HTTPS endpoint and specify which event types you want to receive.

import requests
from uuid import uuid4

BASE_URL = "https://sandbox.api.tote.ai/v1/online-ordering"
headers = {
    "Authorization": "Bearer YOUR_ACCESS_TOKEN",
    "Idempotency-Key": str(uuid4()),
}

response = requests.post(
    f"{BASE_URL}/webhooks",
    headers=headers,
    json={
        "url": "https://your-app.com/webhooks/tote",
        "event_types": [
            "order.created",
            "order.status_changed",
            "order.cancelled",
            "stock.updated"
        ]
    },
)
subscription = response.json()

Response (201 Created):

{
  "id": "wh_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "url": "https://your-app.com/webhooks/tote",
  "event_types": [
    "order.created",
    "order.status_changed",
    "order.cancelled",
    "stock.updated"
  ],
  "status": "ACTIVE",
  "signing_secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "created_at": "2026-02-01T10:00:00Z"
}

IMPORTANT: The signing_secret is only returned in this response. It is not included in any subsequent GET requests. Store it immediately in a secure location (environment variable, secrets manager). You need it to verify webhook signatures.

Listing subscriptions

Retrieve all your webhook subscriptions. Note that signing_secret is NOT included in the response.

response = requests.get(
    f"{BASE_URL}/webhooks",
    headers={"Authorization": "Bearer YOUR_ACCESS_TOKEN"},
)
subscriptions = response.json()

for sub in subscriptions["data"]:
    print(f"{sub['id']} | {sub['status']} | {sub['url']}")
    print(f"  Events: {', '.join(sub['event_types'])}")

Response (200 OK):

{
  "data": [
    {
      "id": "wh_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "url": "https://your-app.com/webhooks/tote",
      "event_types": ["order.created", "order.status_changed", "order.cancelled", "stock.updated"],
      "status": "ACTIVE",
      "created_at": "2026-02-01T10:00:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}

Deleting a subscription

Remove a subscription when you no longer need to receive events at that endpoint. This action cannot be undone.

webhook_id = "wh_a1b2c3d4-e5f6-7890-abcd-ef1234567890"

response = requests.delete(
    f"{BASE_URL}/webhooks/{webhook_id}",
    headers={"Authorization": "Bearer YOUR_ACCESS_TOKEN"},
)
# response.status_code == 204 (No Content)

After deletion, any in-flight deliveries may still arrive at your endpoint. Your webhook handler should gracefully ignore events for unknown or deleted subscriptions.

Multiple subscriptions

You can create multiple subscriptions with different URLs and event type filters. This is useful for routing events to different services in a microservice architecture.

# Order events -> order processing service
requests.post(
    f"{BASE_URL}/webhooks",
    headers={**headers, "Idempotency-Key": str(uuid4())},
    json={
        "url": "https://your-app.com/orders/webhook",
        "event_types": ["order.created", "order.status_changed", "order.cancelled"]
    },
)

# Inventory events -> inventory service
requests.post(
    f"{BASE_URL}/webhooks",
    headers={**headers, "Idempotency-Key": str(uuid4())},
    json={
        "url": "https://your-app.com/inventory/webhook",
        "event_types": ["stock.updated", "menu.changed"]
    },
)

Subscription status

Each subscription has a status field:

StatusDescription
ACTIVESubscription is receiving events normally.
DISABLEDSubscription has been automatically disabled after 50 consecutive delivery failures.

When a subscription is DISABLED, events are no longer delivered to its URL. To resume delivery, create a new subscription with the same URL and event types. The new subscription will receive a fresh signing_secret.

Monitor your subscription status regularly by listing subscriptions and checking for DISABLED entries.

Verifying Signatures

Every webhook delivery includes an X-Tote-Signature header for verifying the event came from Tote and was not tampered with. Always verify signatures before processing events.

Signature format

The X-Tote-Signature header has this format:

X-Tote-Signature: t=1738443000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ComponentDescription
tUnix timestamp (seconds) when the event was sent.
v1HMAC-SHA256 hex digest of the signed payload.

Verification steps

  1. Extract the timestamp (t) and signature (v1) from the header.
  2. Check that the timestamp is within 5 minutes (300 seconds) of the current time. Reject stale events to prevent replay attacks.
  3. Compute the HMAC-SHA256 of {timestamp}.{raw_body} using your signing_secret as the key. The signed payload is the timestamp, a literal dot (.), then the raw request body bytes.
  4. Compare your computed signature to the received v1 value using a constant-time comparison function.

Python

import hashlib
import hmac
import time


def verify_webhook_signature(
    payload: bytes,
    signature_header: str,
    secret: str,
    tolerance: int = 300,
) -> bool:
    """
    Verify a Tote webhook signature.

    Args:
        payload: Raw request body bytes (do NOT parse and re-serialize).
        signature_header: Value of the X-Tote-Signature header.
        secret: Your webhook signing_secret from subscription creation.
        tolerance: Maximum age of the event in seconds (default 5 minutes).

    Returns:
        True if the signature is valid and the timestamp is within tolerance.
    """
    # Parse header: t=1738443000,v1=abc123...
    parts = {}
    for element in signature_header.split(","):
        key, value = element.split("=", 1)
        parts[key.strip()] = value.strip()

    timestamp = parts.get("t")
    received_signature = parts.get("v1")

    if not timestamp or not received_signature:
        return False

    # Check timestamp tolerance (replay protection)
    if abs(time.time() - int(timestamp)) > tolerance:
        return False

    # Compute expected signature: HMAC-SHA256 of "{timestamp}.{raw_body}"
    signed_payload = f"{timestamp}.".encode() + payload
    expected_signature = hmac.new(
        secret.encode(), signed_payload, hashlib.sha256
    ).hexdigest()

    # Constant-time comparison prevents timing attacks
    return hmac.compare_digest(expected_signature, received_signature)

Usage in a Django view:

from django.http import HttpResponse, HttpResponseForbidden
import json
import os

WEBHOOK_SECRET = os.environ["TOTE_WEBHOOK_SECRET"]

def webhook_handler(request):
    # request.body is the raw bytes -- do NOT use request.POST or json.loads() for verification
    signature = request.headers.get("X-Tote-Signature", "")

    if not verify_webhook_signature(request.body, signature, WEBHOOK_SECRET):
        return HttpResponseForbidden("Invalid signature")

    event = json.loads(request.body)
    # ... process event
    return HttpResponse(status=200)

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, secret, tolerance = 300) {
  // Parse header: t=1738443000,v1=abc123...
  const parts = {};
  signatureHeader.split(',').forEach(element => {
    const [key, ...rest] = element.split('=');
    parts[key.trim()] = rest.join('=').trim();
  });

  const timestamp = parts.t;
  const receivedSignature = parts.v1;

  if (!timestamp || !receivedSignature) return false;

  // Check timestamp tolerance (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > tolerance) {
    return false;
  }

  // Compute expected signature: HMAC-SHA256 of "{timestamp}.{raw_body}"
  const signedPayload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison prevents timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

Usage in an Express app:

const express = require('express');
const app = express();

// IMPORTANT: Use express.raw() to preserve the raw body bytes
app.post('/webhooks/tote', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-tote-signature'] || '';
  const rawBody = req.body.toString();

  if (!verifyWebhookSignature(rawBody, signature, process.env.TOTE_WEBHOOK_SECRET)) {
    return res.status(403).send('Invalid signature');
  }

  const event = JSON.parse(rawBody);
  // ... process event
  res.sendStatus(200);
});

Go

package webhooks

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"
)

func VerifyWebhookSignature(rawBody []byte, signatureHeader, secret string, tolerance int64) bool {
	// Parse header: t=1738443000,v1=abc123...
	parts := make(map[string]string)
	for _, element := range strings.Split(signatureHeader, ",") {
		kv := strings.SplitN(strings.TrimSpace(element), "=", 2)
		if len(kv) == 2 {
			parts[kv[0]] = kv[1]
		}
	}

	timestamp, ok := parts["t"]
	if !ok {
		return false
	}
	receivedSignature, ok := parts["v1"]
	if !ok {
		return false
	}

	// Check timestamp tolerance (replay protection)
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}
	if math.Abs(float64(time.Now().Unix()-ts)) > float64(tolerance) {
		return false
	}

	// Compute expected signature: HMAC-SHA256 of "{timestamp}.{raw_body}"
	signedPayload := fmt.Sprintf("%s.%s", timestamp, string(rawBody))
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(signedPayload))
	expectedSignature := hex.EncodeToString(mac.Sum(nil))

	// Constant-time comparison prevents timing attacks
	return hmac.Equal([]byte(expectedSignature), []byte(receivedSignature))
}

func WebhookHandler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	signature := r.Header.Get("X-Tote-Signature")
	secret := "your_signing_secret" // Load from environment variable

	if !VerifyWebhookSignature(body, signature, secret, 300) {
		http.Error(w, "Invalid signature", http.StatusForbidden)
		return
	}

	// ... process event
	w.WriteHeader(http.StatusOK)
}

WARNING: Always verify against the raw request body bytes. Do NOT parse the JSON and re-serialize it -- key ordering, whitespace, or numeric precision differences will cause a signature mismatch. This is the number one webhook integration bug. In Python, use request.body (not json.loads() then json.dumps()). In Node.js, use express.raw(). In Go, use io.ReadAll(r.Body).

Handling Retries

If your endpoint does not return a 2xx response within 30 seconds, Tote retries delivery with exponential backoff.

Retry schedule

AttemptDelay After PreviousTime Since First Attempt
1Immediate0s
25 seconds5s
330 seconds35s
42 minutes2m 35s
515 minutes17m 35s
61 hour1h 17m 35s
74 hours5h 17m 35s

Retry rules

  • Success: Return any 2xx status code within 30 seconds. The event is considered delivered.
  • Failure: Any non-2xx response, connection error, or timeout (>30 seconds) triggers a retry.
  • Exhaustion: After 7 attempts (the initial delivery plus 6 retries), the event is dropped for that subscription.
  • Automatic disabling: After 50 consecutive failed events across any deliveries, the subscription status changes to DISABLED and all further deliveries stop.

Tip: Return 200 immediately and process the event asynchronously in a background queue. This avoids timeouts and ensures retries are only triggered by genuine failures.

Processing Events Idempotently

Network issues and retries mean your endpoint may receive the same event more than once. Your handler must process each event at most once.

Why idempotency matters

Consider this scenario:

  1. Tote sends an order.created event to your endpoint.
  2. Your endpoint processes the event and starts order preparation.
  3. Your endpoint returns 200, but the response is lost due to a network issue.
  4. Tote does not see the 200 and retries the delivery.
  5. Without idempotency, your system starts a duplicate preparation workflow.

Pattern: Deduplicate using event_id

Every event has a unique event_id. Store processed IDs and check before processing.

import redis
import json
import os

redis_client = redis.Redis()
WEBHOOK_SECRET = os.environ["TOTE_WEBHOOK_SECRET"]

EVENT_HANDLERS = {
    "order.created": "handle_order_created",
    "order.status_changed": "handle_order_status_changed",
    "order.cancelled": "handle_order_cancelled",
    "stock.updated": "handle_stock_updated",
    "menu.changed": "handle_menu_changed",
    "location.hours_changed": "handle_location_hours_changed",
}


def webhook_endpoint(request):
    """Complete webhook handler with verification, deduplication, and routing."""

    # Step 1: Verify signature
    signature = request.headers.get("X-Tote-Signature", "")
    if not verify_webhook_signature(request.body, signature, WEBHOOK_SECRET):
        return HttpResponseForbidden("Invalid signature")

    event = json.loads(request.body)
    event_id = event["event_id"]

    # Step 2: Check for duplicate (7-day deduplication window)
    cache_key = f"webhook:processed:{event_id}"
    if redis_client.get(cache_key):
        # Already processed -- return 200 so Tote does not retry
        return HttpResponse(status=200)

    # Step 3: Route to event-type handler
    event_type = event["event_type"]
    handler_name = EVENT_HANDLERS.get(event_type)
    if handler_name:
        handler = globals()[handler_name]
        handler(event["data"])

    # Step 4: Mark as processed (7-day TTL = 604800 seconds)
    redis_client.setex(cache_key, 604800, "1")

    # Step 5: Return 200
    return HttpResponse(status=200)

Alternative: Database deduplication

If you do not use Redis, use a database table with a unique constraint on event_id:

CREATE TABLE processed_webhook_events (
    event_id UUID PRIMARY KEY,
    event_type VARCHAR(50) NOT NULL,
    processed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Clean up events older than 7 days
DELETE FROM processed_webhook_events
WHERE processed_at < NOW() - INTERVAL '7 days';

Check for the event_id before processing. If the INSERT fails with a unique constraint violation, the event was already processed.

Event Payloads

This section shows complete payload examples for the three most common event types, with field-level annotations. For all six event types, see the API reference.

order.created

Fires when a new order is placed via checkout. Use this to start your order preparation workflow.

{
  "event_id": "evt_c3d4e5f6-a7b8-9012-cdef-234567890123",
  "event_type": "order.created",
  "created_at": "2026-02-01T14:00:00Z",
  "data": {
    "order_id": "f9a8b7c6-d5e4-3210-fedc-ba9876543210",
    "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
    "status": "PENDING",
    "handoff_mode": "CURBSIDE",
    "total": {
      "amount": 1945,
      "currency": "USD"
    },
    "created_at": "2026-02-01T14:00:00Z"
  }
}
FieldDescription
data.order_idThe new order's ID. Fetch full details with GET /orders/{orderId}.
data.location_idWhich store the order was placed at.
data.statusAlways PENDING at creation -- payment has not been submitted yet.
data.handoff_modeHow the customer wants to receive the order (PICKUP, CURBSIDE, DELIVERY, or KIOSK).
data.totalOrder total in cents. 1945 = $19.45.

order.status_changed

Fires when any of the three status fields changes on an order. This is the primary event for order tracking.

{
  "event_id": "evt_d4e5f6a7-b8c9-0123-def0-345678901234",
  "event_type": "order.status_changed",
  "created_at": "2026-02-01T14:30:00Z",
  "data": {
    "order_id": "f9a8b7c6-d5e4-3210-fedc-ba9876543210",
    "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
    "previous_status": "CONFIRMED",
    "current_status": "COMPLETED",
    "previous_fulfillment_status": "READY_FOR_PICKUP",
    "current_fulfillment_status": "FULFILLED",
    "previous_payment_status": "PAID",
    "current_payment_status": "PAID",
    "updated_at": "2026-02-01T14:30:00Z"
  }
}
FieldDescription
data.previous_status / data.current_statusThe overall order lifecycle status before and after the change.
data.previous_fulfillment_status / data.current_fulfillment_statusPhysical preparation and handoff status. This is the field to display in customer-facing tracking UI.
data.previous_payment_status / data.current_payment_statusAggregate payment state. Check this for payment confirmation.

The event includes both previous and current values for all three status fields, so you can determine exactly what changed without fetching the order.

stock.updated

Fires when item availability changes at a location. Events are batched -- one event can report multiple items.

{
  "event_id": "evt_f6a7b8c9-d0e1-2345-f012-567890123456",
  "event_type": "stock.updated",
  "created_at": "2026-02-01T13:45:00Z",
  "data": {
    "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
    "items": [
      {
        "item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "name": "Build Your Own Sub Sandwich",
        "previous_status": "IN_STOCK",
        "current_status": "LOW_STOCK",
        "updated_at": "2026-02-01T13:45:00Z"
      },
      {
        "item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567",
        "name": "Bottled Water",
        "previous_status": "IN_STOCK",
        "current_status": "OUT_OF_STOCK",
        "updated_at": "2026-02-01T13:45:00Z"
      }
    ]
  }
}

Each item includes previous_status and current_status using the three-level availability enum: IN_STOCK, LOW_STOCK, OUT_OF_STOCK. Use current_status to update your menu display -- hide or gray out items that are OUT_OF_STOCK, and optionally show a "limited availability" indicator for LOW_STOCK.

Other event types

The remaining three event types follow the same envelope structure:

order.cancelled -- Includes order_id, location_id, reason, and cancelled_at. See the Order Tracking guide for cancellation details.

menu.changed -- Includes location_id, version_hash, change_type (ITEMS_MODIFIED, CATEGORIES_MODIFIED, or FULL_UPDATE), and changed_at. Compare version_hash with your cached value to determine if a re-sync is needed. See the Menu Synchronization guide for sync strategies.

location.hours_changed -- Includes location_id, full previous_hours and current_hours arrays, effective_date, and changed_at. The complete hours arrays are included so you can diff them without fetching the location.

Polling Fallback

Webhooks are best-effort delivery. Always implement polling as a safety net to catch events that may have been missed due to delivery failures, network issues, or subscription downtime.

Webhook-to-polling mapping

Every webhook event type has a corresponding polling endpoint:

Webhook EventPolling EndpointWhat to Compare
order.createdGET /orders?date_from={last_poll}New orders since last poll
order.status_changedGET /orders/{orderId}status, fulfillment_status, payment_status
order.cancelledGET /orders/{orderId}status == CANCELLED
stock.updatedGET /locations/{locationId}/inventoryavailability_status per item
menu.changedGET /locations/{locationId}/menu/metadataversion_hash
location.hours_changedGET /locations/{locationId}business_hours array

Use webhooks as the primary delivery mechanism and polling as a reconciliation layer:

  • When webhooks are working: Poll every 5 minutes as a background reconciliation check. Compare the polling results with what your webhook handler has already processed.
  • When webhooks fail: If you detect missed events (e.g., your subscription status is DISABLED), switch to polling at 30-second intervals until the subscription is restored.

For detailed polling implementation with exponential backoff, see the Order Tracking guide.

Testing Webhooks Locally

You can test your webhook verification logic locally without waiting for real events.

Generate a test signature

Use Python to compute a valid signature for a test payload:

import hashlib
import hmac
import json
import time

secret = "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
timestamp = str(int(time.time()))

payload = json.dumps({
    "event_id": "evt_test-0001-0001-0001-000000000001",
    "event_type": "order.created",
    "created_at": "2026-02-01T14:00:00Z",
    "data": {
        "order_id": "f9a8b7c6-d5e4-3210-fedc-ba9876543210",
        "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
        "status": "PENDING",
        "handoff_mode": "PICKUP",
        "total": {"amount": 1945, "currency": "USD"},
        "created_at": "2026-02-01T14:00:00Z"
    }
})

signed_payload = f"{timestamp}.{payload}"
signature = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()

print(f"X-Tote-Signature: t={timestamp},v1={signature}")
print(f"Payload: {payload}")

Send a test event with curl

Use the generated signature to send a test event to your local handler:

curl -X POST http://localhost:8000/webhooks/tote \
  -H "Content-Type: application/json" \
  -H "X-Tote-Signature: t=1738443000,v1=YOUR_GENERATED_SIGNATURE" \
  -d '{"event_id":"evt_test-0001-0001-0001-000000000001","event_type":"order.created","created_at":"2026-02-01T14:00:00Z","data":{"order_id":"f9a8b7c6-d5e4-3210-fedc-ba9876543210","location_id":"b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d","status":"PENDING","handoff_mode":"PICKUP","total":{"amount":1945,"currency":"USD"},"created_at":"2026-02-01T14:00:00Z"}}'

Your handler should return 200 if the signature is valid.

Testing with real sandbox events

To receive real webhook events from the sandbox environment on your local machine, use a tunneling tool to expose your local server:

# Using ngrok
ngrok http 8000

# Using cloudflared
cloudflared tunnel --url http://localhost:8000

Then create a webhook subscription using the tunnel URL as the endpoint. Sandbox events (from test orders, menu changes, etc.) will be delivered to your local server.

Best Practices

  • Always verify signatures before processing. Never skip verification, even in development. A compromised endpoint can inject fake events into your system.
  • Process events idempotently using event_id. Store processed IDs with a 7-day TTL and check before processing. This protects against duplicate deliveries from retries.
  • Return 200 quickly, defer heavy work. Acknowledge the event immediately and push processing to a background queue. This avoids timeouts and unnecessary retries.
  • Implement polling fallback alongside webhooks. Webhooks are best-effort. Poll every 5 minutes as a reconciliation check to catch missed events.
  • Store signing_secret securely. Use environment variables or a secrets manager. Never hardcode secrets in source code or log them.
  • Monitor subscription health. Periodically list your subscriptions and check for DISABLED status. A disabled subscription means you are missing events.
  • Use event-type filtering. Only subscribe to the event types you need. This reduces traffic to your endpoint and simplifies your handler logic.
  • Handle unknown event types gracefully. If Tote adds new event types in the future, your handler should return 200 for unrecognized types rather than returning an error (which would trigger retries).

Next Steps