Masheev webhooks
Official Agent Skills for integrating Masheev into your applications
npx -y skills add masheev/skills --skill masheev-webhooksAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when setting up, verifying, or handling Masheev webhook events in your backend. Covers creating webhook subscriptions, HMAC-SHA256 signature verification with timingSafeEqual, raw body preservation before JSON parsing, idempotent event handling, retry logic, and all 11 event types (conversation.created, message.received, contact.updated, etc.). Use this skill whenever someone asks to "handle Masheev events", "set up webhooks", "verify webhook signatures", or wants real-time notifications from Masheev. Also use when debugging webhook delivery failures or missed events.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
6.9 KB, as published. Nobody here has run it
Masheev Webhooks
Receive real-time HTTP callbacks when events happen in Masheev. Every webhook is signed with HMAC-SHA256 so you can verify it came from Masheev.
Quick Start
1. Create a Webhook (Dashboard or API)
Via the Masheev dashboard: Settings > Developers > Webhooks > Create webhook
Or via API:
import { apiClient } from "@masheev/client/server";
await apiClient.webhooks.create.mutate({
orgId: "org_...",
name: "My App Events",
url: "https://myapp.com/api/webhooks/masheev",
events: ["conversation.created", "message.received", "contact.updated"],
});
2. Handle and Verify the Webhook
// Next.js App Router — app/api/webhooks/masheev/route.ts
import crypto from "node:crypto";
const WEBHOOK_SECRET = process.env.MASHEEV_WEBHOOK_SECRET!;
export async function POST(request: Request) {
// Step 1: Get raw body BEFORE parsing JSON
const rawBody = await request.text();
const signature = request.headers.get("x-webhook-signature");
if (!signature) {
return new Response("Missing signature", { status: 401 });
}
// Step 2: Verify HMAC-SHA256 signature
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
// Step 3: Parse and handle the event
const event = JSON.parse(rawBody);
switch (event.type) {
case "message.received":
await handleNewMessage(event.payload);
break;
case "conversation.created":
await handleNewConversation(event.payload);
break;
case "contact.updated":
await handleContactUpdate(event.payload);
break;
}
// Step 4: Return 200 quickly (Masheev retries on non-2xx)
return new Response("OK", { status: 200 });
}
Signature Verification
Every webhook includes an x-webhook-signature header containing the HMAC-SHA256 hex digest of the raw request body.
Correct (timing-safe comparison):
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const isValid = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
Wrong (vulnerable to timing attacks):
// NEVER do this
if (signature === expected) { ... }
Wrong (body already parsed — signature won't match):
// NEVER do this — JSON.stringify reorders keys, changes whitespace
const body = await request.json();
const rawBody = JSON.stringify(body); // This is NOT the original raw body
Framework-Specific Raw Body Access
| Framework | Raw Body Access |
|---|---|
| Next.js App Router | await request.text() |
| Next.js Pages Router | Set config.api.bodyParser = false, read req as stream |
| Express | app.use("/webhooks", express.raw({ type: "application/json" })) |
| Hono | await c.req.text() |
| Fastify | fastify.addContentTypeParser("application/json", { parseAs: "string" }, ...) |
See references/frameworks.md for complete examples in each framework.
Event Types
| Event | Trigger | Key Payload Fields |
|---|---|---|
conversation.created | New conversation started | conversationId, contactId, inboxId, channel |
conversation.updated | Status, priority, or assignment changed | conversationId, status, priority, assigneeId |
conversation.closed | Conversation resolved | conversationId, resolvedAt, outcome |
conversation.assigned | Assigned to agent or AI | conversationId, assigneeType, assigneeId |
message.received | Message from contact | messageId, conversationId, role: "contact", content |
message.sent | Message sent by agent or AI | messageId, conversationId, role, content, status |
message.failed | Message delivery failed | messageId, conversationId, status: "failed", error |
contact.created | New contact identified | contactId, name, email, phone |
contact.updated | Contact info changed | contactId, changed fields |
escalation.created | Conversation escalated to human | escalationId, conversationId, reason |
escalation.resolved | Escalation handled | escalationId, conversationId, status: "resolved" |
See references/events.md for complete payload schemas.
Webhook Payload Format
Every webhook delivery has this shape:
{
id: string; // Unique event ID (for idempotency)
type: string; // e.g., "conversation.created"
timestamp: number; // Unix ms
payload: { ... }; // Event-specific data
}
Idempotent Handling
Masheev may deliver the same event more than once (retries on timeout/error). Use the id field to deduplicate:
// Simple in-memory dedup (use Redis/DB for production)
const processed = new Set<string>();
async function handleWebhook(event: WebhookEvent) {
if (processed.has(event.id)) return; // Already handled
processed.add(event.id);
// Process the event...
}
Retry Behavior
- Masheev retries on non-2xx responses and timeouts
- Retry schedule: 1 min, 5 min, 30 min, 2 hours, 24 hours
- Webhooks are paused after 7 consecutive days of failures
- Respond with 200 within 30 seconds to avoid timeout retries
Testing Webhooks
# Test delivery from the Masheev dashboard
# Settings > Developers > Webhooks > [your webhook] > Send Test
# Or via API
await apiClient.webhooks.test.mutate({
id: "wh_...",
orgId: "org_...",
});
For local development, use a tunnel:
# ngrok
ngrok http 3000
# Then update your webhook URL to the ngrok URL
Managing Webhooks
// List all webhooks
const webhooks = await apiClient.webhooks.list.query({ orgId: "org_..." });
// Update events
await apiClient.webhooks.update.mutate({
id: "wh_...",
orgId: "org_...",
events: ["message.received", "message.sent"],
});
// Rotate the signing secret
await apiClient.webhooks.regenerateSecret.mutate({
id: "wh_...",
orgId: "org_...",
});
// Delete
await apiClient.webhooks.delete.mutate({ id: "wh_...", orgId: "org_..." });