Destinations

Destinations define where event notifications are delivered. When something happens to a device in your workspace — a connection, a status change, a command result — Texture can notify your systems in real time.

TypeDeliveryUse case
WebhookHTTP POST to your URLProduction integrations, automation pipelines
EmailEmail to an addressAlerts for ops teams, stakeholders

Destination object

{
  "id": "dest_abc123def456",
  "workspaceId": "ws_abc123def456",
  "label": "Production Webhook",
  "destinationType": "WEBHOOK",
  "deviceType": "ALL",
  "eventTypes": ["DEVICE_UPDATED", "DEVICE_CONNECTED"],
  "url": "https://example.com/webhook",
  "email": null,
  "phoneNumber": null,
  "secret": "whsec_my-webhook-secret",
  "headers": { "X-Custom-Header": "value" },
  "includeWeather": false,
  "deactivated": false,
  "deactivatedAt": null,
  "deactivatedReason": null,
  "createdAt": "2025-06-01T00:00:00.000Z",
  "updatedAt": "2025-06-01T00:00:00.000Z"
}

Event types

EventDescription
COMMAND_FAILEDA device command failed
COMMAND_SCHEDULEDA device command was scheduled
COMMAND_SUCCEEDEDA device command succeeded
COMMAND_OVERRIDDENA device command was overridden
CONNECTION_FAILEDA device connection attempt failed
CUSTOMER_CREATEDA customer was created
CUSTOMER_DELETEDA customer was deleted
CUSTOMER_UPDATEDA customer was updated
DEVICE_CONNECTEDA device came online
DEVICE_DISCONNECTEDA device went offline
DEVICE_DISCOVEREDA new device was discovered
DEVICE_UPDATEDDevice data was updated
ENROLLMENT_APPROVEDAn enrollment was approved
ENROLLMENT_REJECTEDAn enrollment was rejected
ENROLLMENT_SUBMITTEDAn enrollment was submitted
SITE_CREATEDA site was created
SITE_DELETEDA site was deleted
SITE_UPDATEDA site was updated
ALERT_CREATEDAn alert was created
ALERT_UPDATEDAn alert was updated

Device type scopes

Filter which device types trigger notifications:

ScopeDescription
ALLAll device types
BATTERIESBattery storage systems
CHARGERSEV chargers
SOLAR_INVERTERSSolar inverters
THERMOSTATSSmart thermostats
VEHICLESElectric vehicles
OTHEROther device types
UNKNOWNUnknown device types

List destinations

GET /v1/destinations

Query parameters:

ParameterTypeDefaultDescription
pageinteger1Page number
perPageinteger20Items per page

Response: 200 OK — Paginated list of destination objects.


Get destination

GET /v1/destinations/:id

Response: 200 OK — Returns the destination object.


Create webhook destination

POST /v1/destinations/webhooks

Request body:

FieldTypeRequiredDescription
labelstringYesHuman-readable label (1–255 chars)
urlstringYesWebhook delivery URL (must be HTTPS)
eventTypesstring[]YesAt least one event type
deviceTypestringYesDevice type scope
secretstringNoHMAC signing secret for signature verification. If omitted, Texture generates one for you.
includeWeatherbooleanNoInclude weather data in event payloads
headersobjectNoCustom HTTP headers sent with each delivery
{
  "label": "Production Webhook",
  "url": "https://example.com/webhook",
  "eventTypes": ["DEVICE_UPDATED", "DEVICE_CONNECTED"],
  "deviceType": "ALL",
  "secret": "whsec_my-secret-for-hmac-verification",
  "headers": { "X-Custom-Header": "value" }
}

Response: 201 Created — Returns the created destination object.


Create email destination

POST /v1/destinations/emails
FieldTypeRequiredDescription
labelstringYesHuman-readable label (1–255 chars)
emailstringYesDelivery email address
eventTypesstring[]YesAt least one event type
deviceTypestringYesDevice type scope

Response: 201 Created


Update destination

PATCH /v1/destinations/:id

All fields are optional — provide at least one.

FieldTypeDescription
labelstringNew label (1–255 chars)
eventTypesstring[]New event types (min 1)
deviceTypestringNew device type scope
urlstringNew webhook URL (webhook only)
emailstringNew email (email only)
secretstringNew signing secret (webhook only)
includeWeatherbooleanInclude weather data (webhook only)
headersobjectNew custom headers (webhook only)

Response: 200 OK — Returns the updated destination object.


Delete destination

DELETE /v1/destinations/:id

Response: 204 No Content


Toggle destination

Enable or disable a destination without deleting it.

PUT /v1/destinations/:id/enabled
FieldTypeRequiredDescription
enabledbooleanYestrue to enable, false to disable

Response: 200 OK — Returns the updated destination object.


Webhook signature verification

When you configure a webhook destination with a secret, Texture signs every delivery so you can verify it came from us and hasn't been tampered with.

How it works

  1. Texture computes an HMAC-SHA256 hash of JSON.stringify(payload) using your destination's secret.
  2. The resulting hex digest is sent in the Texture-Signature header.
  3. A Texture-Timestamp header contains the Unix millisecond timestamp of when the request was sent.

Verifying signatures

Compute the same HMAC on your end and compare it to the header value.

import crypto from "crypto";
 
function verifyWebhookSignature(rawBody, secret, signatureHeader) {
  const computed = crypto
    .createHmac("sha256", secret)
    .update(rawBody) // Use the raw JSON body string — do not re-serialize
    .digest("hex");
 
  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(computed, "hex"),
    Buffer.from(signatureHeader, "hex")
  );
}
 
// In your Express handler:
app.post("/webhooks/texture", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["texture-signature"];
  const timestamp = req.headers["texture-timestamp"];
 
  if (!verifyWebhookSignature(req.body, YOUR_WEBHOOK_SECRET, signature)) {
    return res.status(401).send("Invalid signature");
  }
 
  // Optionally check timestamp to prevent replay attacks
  const ageMs = Date.now() - Number(timestamp);
  if (ageMs > 5 * 60 * 1000) { // 5 minutes
    return res.status(401).send("Webhook too old");
  }
 
  // Process the event
  const event = JSON.parse(req.body);
  console.log("Received event:", event.type);
  res.status(200).send("OK");
});
import hmac
import hashlib
import time
 
def verify_webhook_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
    computed = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(computed, signature_header)
 
# In your Flask handler:
@app.route("/webhooks/texture", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("Texture-Signature")
    timestamp = request.headers.get("Texture-Timestamp")
 
    if not verify_webhook_signature(request.data, YOUR_WEBHOOK_SECRET, signature):
        return "Invalid signature", 401
 
    # Check timestamp freshness (timestamp is Unix milliseconds)
    age_ms = int(time.time() * 1000) - int(timestamp)
    if age_ms > 5 * 60 * 1000:
        return "Webhook too old", 401
 
    event = request.get_json()
    print(f"Received event: {event['type']}")
    return "OK", 200

Important considerations

  • Use the raw request body for HMAC computation. Parsing and re-serializing JSON may change whitespace or key order, producing a different hash.
  • Use constant-time comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks.
  • Check the timestamp to guard against replay attacks. The Texture-Timestamp header is a Unix millisecond timestamp. Reject webhooks older than 5 minutes.
  • HTTP headers are case-insensitive. Your framework may lowercase the header name to texture-signature. Check your framework's documentation.
Tip

Texture provides sample webhook applications in several languages in our open-source examples repository.

Testing webhooks locally

Webhooks are sent from Texture's servers, so localhost URLs won't work. Use a tunneling tool like ngrok to expose your local server:

ngrok http 3000
# Use the ngrok URL as your webhook destination