Documentation

Español

Webhooks

Receive signed events from Mico when a summary is ready or a commitment is created or completed.

Webhooks let your own server react to your meetings: post a summary to a channel, open a ticket for each commitment, or update a CRM. Mico's servers send them, so they arrive even while the desktop app is closed.

Webhooks are part of the Flow plan. Configure them in Settings → Developers → Webhooks.

Set up an endpoint

  1. Open Settings → Developers → Webhooks.
  2. Enter an https:// URL on a public domain, choose the events, and decide whether to include content.
  3. Select Add endpoint and copy the signing secret. It starts with whsec_ and is shown only once.
  4. Select Send test event to check that your server receives and verifies a webhook.test event.

You can pause, edit, rotate the secret of, or delete an endpoint from the same page. Each endpoint lists its recent deliveries and their result.

Events

EventFires when
meeting.summary_readyA meeting summary gets content for the first time, or Mico regenerates it.
action_item.createdA new commitment appears in a meeting or note.
action_item.completedA commitment is marked as done.
webhook.testYou select Send test event. Always delivered, whatever events you chose.

Mico detects events on its servers, from what your devices sync. Syncing the same note again does not repeat an event. Editing a summary or the text of a commitment does not send one either.

Events wait one minute (summaries) or two minutes (commitments) before they are sent, so that a commitment you are still typing arrives complete. If the summary or commitment is deleted in that time, or the commitment is no longer done, the event is not sent.

Summaries written on your device with your own AI provider send meeting.summary_ready the first time only. A summary or commitment that first syncs more than seven days after it was created does not send meeting.summary_ready or action_item.created.

Payload

Every request is a JSON POST with this envelope:

{
  "type": "action_item.completed",
  "timestamp": "2026-10-06T10:02:00.000Z",
  "data": { ... }
}

timestamp is when the event happened. By default data carries IDs, titles, and times, never the text of your notes.

meeting.summary_ready

{
  "type": "meeting.summary_ready",
  "timestamp": "2026-10-06T10:00:00.000Z",
  "data": {
    "meeting": {
      "id": "3f2b0c1e-...",
      "title": "Kickoff with Acme",
      "started_at": "2026-10-06T09:00:00.000Z",
      "ended_at": "2026-10-06T09:45:00.000Z"
    },
    "summary": {
      "id": "3f2b0c1e-...-ai-notes",
      "kind": "summary",
      "template_id": null,
      "title": "Kickoff with Acme",
      "updated_at": "2026-10-06T09:59:41.000Z"
    }
  }
}

kind is summary, or template_output when the summary used a template, in which case template_id names it.

action_item.created and action_item.completed

{
  "type": "action_item.created",
  "timestamp": "2026-10-06T10:00:00.000Z",
  "data": {
    "action_item": {
      "id": "3f2b0c1e-...-ai-item-0",
      "status": "todo",
      "due_at": "2026-10-10T00:00:00.000Z",
      "completed_at": null,
      "created_at": "2026-10-06T09:59:41.000Z",
      "updated_at": "2026-10-06T09:59:41.000Z"
    },
    "meeting": {
      "id": "3f2b0c1e-...",
      "title": "Kickoff with Acme",
      "started_at": "2026-10-06T09:00:00.000Z",
      "ended_at": "2026-10-06T09:45:00.000Z"
    }
  }
}

status is todo, in_progress, or done. meeting is null for a commitment that does not belong to a meeting. completed_at can be null even when the status is done.

With content

Turn on Include content for an endpoint to add the text:

  • data.summary.content.markdown: the summary as Markdown. Headings, lists, and checkboxes are kept; inline formatting such as bold and links is not. Capped at 100,000 characters.
  • data.action_item.text and data.action_item.assignee: the commitment and the person it is assigned to, or null.

Only turn it on for a server you trust: the content of your meetings leaves Mico.

webhook.test

{
  "type": "webhook.test",
  "timestamp": "2026-10-06T10:00:00.000Z",
  "data": {
    "endpoint_id": "6a1f...",
    "message": "This is a test event from Mico."
  }
}

Headers

Mico follows the Standard Webhooks specification.

HeaderContent
webhook-idUnique event ID, such as msg_0f6b2a5e1c2d4e5f8a9b0c1d2e3f4a5b. The same on every retry.
webhook-timestampUnix time in seconds when this attempt was signed.
webhook-signaturev1,<base64>, a space-separated list. Two signatures during a secret rotation.
content-typeapplication/json
user-agentMico-Webhooks/1.0 (+https://www.meetmico.ai/docs/reference/webhooks/)

Verify signatures

The signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_, not the secret as text.

The simplest way is an official Standard Webhooks library:

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.MICO_WEBHOOK_SECRET); // "whsec_..."

// rawBody must be the exact bytes received, before any JSON parsing.
const event = wh.verify(rawBody, {
  "webhook-id": request.headers["webhook-id"],
  "webhook-timestamp": request.headers["webhook-timestamp"],
  "webhook-signature": request.headers["webhook-signature"],
});

Without a library, in Node.js:

import crypto from "node:crypto";

function verifyMicoWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = String(headers["webhook-signature"] ?? "").split(" ");

  // Reject old or future timestamps to stop replays.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 5 * 60) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();

  return signatures.some((entry) => {
    const [version, signature] = entry.split(",");
    if (version !== "v1" || !signature) return false;
    const received = Buffer.from(signature, "base64");
    return (
      received.length === expected.length &&
      crypto.timingSafeEqual(received, expected)
    );
  });
}

Store each webhook-id you process and ignore repeats: a delivery can arrive more than once.

Respond and retries

Answer with any 2xx status within 10 seconds. Do the slow work after responding. Mico does not read the response body and does not follow redirects: a 3xx counts as a failure.

A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours: eight attempts over about 27 hours. Retries carry the same webhook-id and body, with a new timestamp and signature. A test event is attempted once.

An endpoint that fails 15 times in a row and has not had a successful delivery for more than 24 hours is turned off, and its pending deliveries are dropped. Settings shows why. Fix your server, send a test event, and turn the endpoint back on.

Rotate the secret

Select Rotate secret on an endpoint to get a new one, shown once. For the next 24 hours Mico signs every request with both the new and the previous secret, so you can update your server without losing events.

Security and limits

  • Endpoints must use https:// and a public domain name. IP addresses, localhost, and names such as *.local or *.internal are rejected.
  • Before each delivery Mico resolves the domain and does not send if any address is private, loopback, link-local, or otherwise not on the public internet.
  • Up to 10 endpoints per account, and 10 test events per endpoint per hour.
  • Delivery history is kept for 30 days.
  • Webhooks stop when the account no longer has the Flow plan, and resume when it has it again. Events that happened in between are not sent.