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 |
Alert events and workspace visibility#
ALERT_CREATED and ALERT_UPDATED destinations receive notifications when alerts change in a workspace. Delivery is separate from whether the alert is stored.
| Layer | Always happens? | Respects workspace alert config? |
|---|---|---|
| Alert captured in Texture | Yes | No |
| Event archived for the workspace | Yes | No |
| Webhook / email / SMS / Kafka fanout | Only if destination is enabled and subscribed | Yes (after first config save) |
Workspace alert configuration controls which alerts appear in the Dashboard and which alert events are delivered to destinations. Until an admin saves alert settings for the workspace, all alerts that match your destination eventTypes are delivered—same as historical behavior.
After configuration is saved:
| Config state | ALERT_CREATED / ALERT_UPDATED delivery |
|---|---|
| No saved config | Delivered (no visibility filter) |
| Master switch disabled | Not delivered |
| Severity disabled (e.g. INFO off) | Not delivered for that severity |
| OEM hidden | Not delivered for alerts from that provider |
| Alert passes all rules | Delivered |
Filtered-out alerts are still stored and remain available for audit. Your webhook or inbox simply does not receive a payload for them.
If Texture cannot read the saved configuration temporarily, the platform fails open: destinations continue to receive alert events rather than silently stopping delivery.
Configure visibility in the Dashboard under Settings → Alerts. See Alert configuration and OEM alert reference for setup details and provider keys. List and lifecycle operations are available via the Alerts REST API.
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