PlugLog
‹ Webhooks & Zapier

Developers

Webhooks

PlugLog sends a signed JSON POST to your https URL for each event you pick, up to 5 URLs per account. Add one on Integrations › Webhooks & Zapier. No code? Use PlugLog on Zapier: the same events, with a Zapier key from that page.

Events

  • ⚡ Charge started
    Code
    charge.started
    Sent when
    A charge session starts.
  • ✅ Charge finished
    Code
    charge.finished
    Sent when
    A charge is over and saved (once per charge).
  • ⚠️ Charging stopped early
    Code
    charge.stopped
    Sent when
    A charge ended 5 or more points below the vehicle's charge limit (only when the limit is known).
  • 🔋 Battery reached a %
    Code
    battery.reached
    Sent when
    The battery goes up to the % you pick (default 80%).
  • 🪫 Battery below a %
    Code
    battery.low
    Sent when
    The battery goes down to the % you pick (default 20%), by default only while not plugged in.
  • 📡 Not heard from in N hours
    Code
    vehicle.silent
    Sent when
    No data from the vehicle for the hours you pick (default 12), once per silence.
  • 🚗 Drive started
    Code
    drive.started
    Sent when
    A drive starts.
  • 🏁 Drive ended
    Code
    drive.ended
    Sent when
    A drive is over and saved (once per drive); optionally business drives only.
  • 🗺️ Trip arrived
    Code
    trip.arrived
    Sent when
    A planned trip's last stop is reached.
  • 🏅 Badge earned
    Code
    badge.earned
    Sent when
    A badge, or its next tier, is earned.
  • 📅 Monthly cost summary
    Code
    summary.monthly
    Sent when
    On the 1st of the month: last month's totals for each vehicle.

Each event goes once per webhook and object: a charge saved again never sends a second charge.finished.

Payload

Every message has id (unique per event), event, at (UTC), vehicle and the one object for its event: charge, drive, battery, trip, badge, silence or summary. Distances and efficiency are in your account's units. Send test adds "test": true.

{
  "id": "evt_3kq9ZbVn1xQe0pTa7cWm",
  "event": "charge.finished",
  "at": "2026-10-30T22:14:05Z",
  "vehicle": {
    "id": "sample-vehicle",
    "name": "Silverado EV",
    "make": "Chevrolet",
    "model": "Silverado EV",
    "year": 2024
  },
  "charge": {
    "id": "sample-charge",
    "startedAt": "2026-10-30T17:14:05Z",
    "endedAt": "2026-10-30T22:14:05Z",
    "startPercent": 22,
    "endPercent": 80,
    "kwhAdded": 118.4,
    "cost": 14.21,
    "currency": "USD",
    "place": "Home",
    "maxKw": 11.3,
    "url": "https://pluglog.app/charges/sample-charge"
  }
}

Places are names: “Home”, a saved charging location or favorite, the charger's name, or the nearest city (“near Fort Wayne, IN”). Exact coordinates (lat/lon, a drive's start/end) are only sent when you turn them on for that webhook, and never inside your privacy zones.

Headers

  • Content-Type: application/json
  • User-Agent: PlugLog-Webhooks/1
  • X-PlugLog-Event: the event's code
  • X-PlugLog-Delivery: this delivery's id, the same on every retry
  • X-PlugLog-Signature: t=<unix seconds>,v1=<hex>: HMAC-SHA256 of <t>.<raw body> with your signing secret

Answering and retries

Answer with any 2xx within 10 seconds. Anything else (a redirect too: they aren't followed) is tried again after 1 min, 5 min, 20 min, 1 h, 3 h, 6 h, 12 h: 8 tries in all. A URL failing for a whole day is turned off and you're emailed; turn it back on once it's fixed. Every delivery is in its webhook's list for 30 days, with its payload and Resend.

Verifying the signature

Check it on the raw body, before parsing it, with the webhook's signing secret (whsec_…, shown once when you add it). Refuse old timestamps so a copied message can't be sent again.

Node.js

import crypto from "node:crypto";

// rawBody: the request body exactly as it arrived (a string or Buffer), before any JSON parsing.
export function verifyPlugLog(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: keep the raw body for checking.
app.post("/pluglog", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyPlugLog(req.body.toString("utf8"), req.get("X-PlugLog-Signature") ?? "", process.env.PLUGLOG_SECRET)) return res.sendStatus(401);
  const event = JSON.parse(req.body);
  // Seen X-PlugLog-Delivery before? It's a retry: answer 200 again.
  res.sendStatus(200);
});

Python

import hashlib, hmac, time

def verify_pluglog(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts.get("t", ""))
    except ValueError:
        return False
    if abs(time.time() - t) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

# Flask: request.get_data() is the raw body.
# if not verify_pluglog(request.get_data(), request.headers.get("X-PlugLog-Signature", ""), SECRET): abort(401)