Skip to main content

Overview

Webhooks are the recommended way to receive new reviews. When a review is submitted, eEndorsements fires an HTTP POST to your registered endpoint within seconds — no polling required.

Register a webhook

POST /api/v1/partner/webhook

curl -X POST https://app.eendorsements.com/api/v1/partner/webhook \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/reviews",
    "events": ["review.created"],
    "description": "Production review receiver"
  }'

Request body

FieldTypeRequiredDescription
urlstringHTTPS endpoint to receive events
eventsstring[]Event types to subscribe to. Currently: review.created
descriptionstringHuman-readable label

Response

{
  "webhook": {
    "id": "wh_abc123...",
    "url": "https://yourapp.com/webhooks/reviews",
    "events": ["review.created"],
    "is_active": true,
    "created_at": "2026-04-27T18:00:00.000Z",
    "signing_secret": "a3f8b2c1d9e4..."
  }
}
The signing_secret is returned once at registration. Store it securely — you’ll use it to verify every incoming webhook request.

List webhooks

GET /api/v1/partner/webhook

curl https://app.eendorsements.com/api/v1/partner/webhook \
  -H "x-api-key: YOUR_API_KEY"

Delete a webhook

DELETE /api/v1/partner/webhook?id={webhook_id}

curl -X DELETE "https://app.eendorsements.com/api/v1/partner/webhook?id=wh_abc123" \
  -H "x-api-key: YOUR_API_KEY"

Receiving events

Event payload

{
  "event": "review.created",
  "timestamp": "1745785200",
  "data": {
    "id": "3f8a2c1d-...",
    "review_number": "R-3F8A2C1D",
    "created_at": "2026-04-27T18:00:00.000Z",
    "rating": 5.0,
    "comment": "Had a great experience with Taylor!",
    "is_verified": true,
    "reviewer": {
      "name": "Amber Martin",
      "email": "amber@example.com"
    },
    "professional": {
      "id": "a1b2c3d4-...",
      "name": "Taylor Hernandez"
    }
  }
}

Request headers

HeaderValue
Content-Typeapplication/json
X-EE-EventEvent type (e.g. review.created)
X-EE-TimestampUnix timestamp (seconds)
X-EE-Signaturesha256=<hmac_hex>

Verifying signatures

Every webhook is signed with HMAC-SHA256 using your signing_secret. Always verify the signature before processing the payload to ensure requests are genuinely from eEndorsements.

Verification algorithm

signature = HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}")
Compare the result (prefixed with sha256=) to the X-EE-Signature header using a constant-time comparison.

Node.js example

import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(signingSecret, rawBody, headers) {
  const timestamp = headers["x-ee-timestamp"];
  const signature = headers["x-ee-signature"];

  // Reject events older than 5 minutes
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) {
    throw new Error("Webhook timestamp too old");
  }

  const expected = "sha256=" + createHmac("sha256", signingSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    throw new Error("Invalid signature");
  }
}

// Express handler
app.post("/webhooks/reviews", express.raw({ type: "application/json" }), (req, res) => {
  verifyWebhook(process.env.EE_WEBHOOK_SECRET, req.body.toString(), req.headers);
  const event = JSON.parse(req.body);
  // process event.data ...
  res.sendStatus(200);
});

Python example

import hmac, hashlib, time

def verify_webhook(signing_secret: str, raw_body: bytes, headers: dict) -> None:
    timestamp  = headers["x-ee-timestamp"]
    signature  = headers["x-ee-signature"]

    # Reject stale events
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp too old")

    expected = "sha256=" + hmac.new(
        signing_secret.encode(),
        f"{timestamp}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")

Retry behaviour

eEndorsements retries failed deliveries (non-2xx response or timeout) with exponential back-off:
AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
After 3 failed attempts, the event is dropped and last_error is recorded on the webhook row. You can inspect this via GET /api/v1/partner/webhook.

Best practices

  • Return 200 quickly — acknowledge receipt immediately, process asynchronously
  • Idempotency — use data.id as an idempotency key; the same review may be delivered more than once during retries
  • HTTPS only — HTTP endpoints are rejected at registration