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
| Field | Type | Required | Description |
|---|
url | string | ✓ | HTTPS endpoint to receive events |
events | string[] | ✓ | Event types to subscribe to. Currently: review.created |
description | string | — | Human-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"
}
}
}
| Header | Value |
|---|
Content-Type | application/json |
X-EE-Event | Event type (e.g. review.created) |
X-EE-Timestamp | Unix timestamp (seconds) |
X-EE-Signature | sha256=<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:
| Attempt | Delay |
|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 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