Skip to content

DocsPanel webhooks

Panel webhooks

Send every application to your own system: a sheet, a staff tool, a bot of yours. You give a panel one HTTPS address. This page is what Mico sends to it.

How it fits together

  1. 1

    You set the address

    In the panel's Messages and after a decision part, in the dashboard or in /panel: an https:// address and, if you like, a value for an auth header.

  2. 2

    Something happens

    An application is sent, accepted or denied. Each of the three can be switched off on its own.

  3. 3

    Mico calls you

    A signed POST within seconds, tried again if your server does not answer.

Webhooks come with the paid plans. Which ones is on the pricing page.

The events

EventSent when
application.submittedAn application was sent, after the AI scored it (or could not) and the review message was posted.
application.acceptedA reviewer, or the AI's automatic decision, accepted it.
application.deniedA reviewer, or the AI's automatic decision, denied it.

How it is delivered

  • A POST with a JSON body. Answer with any 2xx status; the body of your answer is not read.
  • Your server has 10 seconds to answer. Redirects are not followed: give the final address.
  • Tried again when there is no answer, a timeout, 408, 425, 429 or any 5xx: after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours, six tries in all. A Retry-After header in seconds is respected, up to an hour. Any other answer is final and the delivery is marked failed.
  • Switched off after 10 deliveries in a row failed. What was still waiting is not sent, and the server's owner gets a message from Mico. Saving the webhook again, or pressing Turn on, starts it again.
  • The same event can arrive more than once, for example a retry after your server answered too late. Use X-Mico-Delivery, or application.id with event, to recognise a repeat.
  • Every delivery is in the panel's delivery log, in the dashboard and in /panel, for 30 days.

The headers

HeaderValue
Content-Typeapplication/json
User-AgentMico-Webhooks/2.0
X-Mico-EventThe event, for example application.submitted.
X-Mico-DeliveryThe delivery's id: the same on every try of one delivery.
X-Mico-Signaturet=<unix seconds>,v1=<hex>. See Check that it came from Mico.
Your auth headerAuthorization, Authorization: Bearer ... or X-API-Key, when you set one.

The body

application.submitted
{
  "event": "application.submitted",
  "timestamp": "2026-10-13T18:40:12.345678+00:00",
  "application": {
    "id": 1234,
    "panel_id": 56,
    "user_id": 111111111111111111,
    "guild_id": 222222222222222222,
    "panel_name": "Staff applications",
    "panel_color": "#5865F2",
    "total_answers": 5,
    "elapsed_time_seconds": 640,
    "quality_score": 82,
    "ai_detection_score": 9,
    "ai_reasoning": "Clear, specific answers ...",
    "auto_decision": "none",
    "auto_decision_reason": "n/a",
    "answers": [
      { "question": "Why do you want to join?", "answer": "Because ...", "is_identifier": false }
    ]
  }
}
  • answers is on application.submitted only: every question in the panel's order, as it was when the application was sent. An unanswered optional question has "".
  • quality_score, ai_detection_score and ai_reasoning are null when the panel does not use the AI or the month's AI scores are used up. auto_decision is accept, deny or none.
  • application.accepted and application.denied carry decider_id (Mico's own id for an automatic decision), is_auto and reason (the reviewer's note, or null).
  • A test send (the Send a test button) has "test": true and an application.id of null.
  • timestamp is when the event happened: every try of one delivery sends the same bytes.
  • Discord ids are numbers above 2^53. Read them as text if your language loses precision on big numbers.

A Discord webhook address (https://discord.com/api/webhooks/...) gets a short message in the channel instead of this body, with no signature.

Check that it came from Mico

Each panel webhook has a signing secret (whsec_...), shown in the panel's webhook settings in the dashboard, or under Deliveries and secret in /panel. v1 is the HMAC-SHA256, with the secret as the key, of t, a dot, then the raw body bytes. To check a request:

  1. Read the raw body, before any JSON parsing.
  2. Compute HMAC-SHA256(secret, t + "." + body) and compare it with v1 in constant time.
  3. Refuse a t more than 5 minutes from your clock: it is an old request sent again.
Python
import hashlib, hmac, time

def verify(secret: str, signature_header: str, body: bytes) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in signature_header.split(","))
        timestamp, given = int(parts["t"]), parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)
Node.js
const crypto = require("crypto");

function verify(secret, signatureHeader, rawBody) {  // rawBody: a Buffer
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${t}.`), rawBody])).digest("hex");
  return expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

New secret makes a new one at once, and the old one stops working, so give the new one to your server right away.