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.
| Type | Delivery | Use case |
|---|---|---|
| Webhook | HTTP POST to your URL | Production integrations, automation pipelines |
| Email to an address | Alerts 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
| Event | Description |
|---|---|
COMMAND_FAILED | A device command failed |
COMMAND_SCHEDULED | A device command was scheduled |
COMMAND_SUCCEEDED | A device command succeeded |
COMMAND_OVERRIDDEN | A device command was overridden |
CONNECTION_FAILED | A device connection attempt failed |
CUSTOMER_CREATED | A customer was created |
CUSTOMER_DELETED | A customer was deleted |
CUSTOMER_UPDATED | A customer was updated |
DEVICE_CONNECTED | A device came online |
DEVICE_DISCONNECTED | A device went offline |
DEVICE_DISCOVERED | A new device was discovered |
DEVICE_UPDATED | Device data was updated |
ENROLLMENT_APPROVED | An enrollment was approved |
ENROLLMENT_REJECTED | An enrollment was rejected |
ENROLLMENT_SUBMITTED | An enrollment was submitted |
SITE_CREATED | A site was created |
SITE_DELETED | A site was deleted |
SITE_UPDATED | A site was updated |
ALERT_CREATED | An alert was created |
ALERT_UPDATED | An alert was updated |
Device type scopes
Filter which device types trigger notifications:
| Scope | Description |
|---|---|
ALL | All device types |
BATTERIES | Battery storage systems |
CHARGERS | EV chargers |
SOLAR_INVERTERS | Solar inverters |
THERMOSTATS | Smart thermostats |
VEHICLES | Electric vehicles |
OTHER | Other device types |
UNKNOWN | Unknown device types |
List destinations
GET /v1/destinations
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
perPage | integer | 20 | Items 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:
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Human-readable label (1–255 chars) |
url | string | Yes | Webhook delivery URL (must be HTTPS) |
eventTypes | string[] | Yes | At least one event type |
deviceType | string | Yes | Device type scope |
secret | string | No | HMAC signing secret for signature verification. If omitted, Texture generates one for you. |
includeWeather | boolean | No | Include weather data in event payloads |
headers | object | No | Custom 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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Human-readable label (1–255 chars) |
email | string | Yes | Delivery email address |
eventTypes | string[] | Yes | At least one event type |
deviceType | string | Yes | Device type scope |
Response: 201 Created
Update destination
PATCH /v1/destinations/:id
All fields are optional — provide at least one.
| Field | Type | Description |
|---|---|---|
label | string | New label (1–255 chars) |
eventTypes | string[] | New event types (min 1) |
deviceType | string | New device type scope |
url | string | New webhook URL (webhook only) |
email | string | New email (email only) |
secret | string | New signing secret (webhook only) |
includeWeather | boolean | Include weather data (webhook only) |
headers | object | New 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
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | true 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
- Texture computes an HMAC-SHA256 hash of
JSON.stringify(payload)using your destination'ssecret. - The resulting hex digest is sent in the
Texture-Signatureheader. - A
Texture-Timestampheader 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", 200Important 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.timingSafeEqualin Node.js,hmac.compare_digestin Python) to prevent timing attacks. - Check the timestamp to guard against replay attacks. The
Texture-Timestampheader 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.
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