Connect to Litmus over MCP
How to let your users connect their Litmus account to your product, and call Litmus's tools on their behalf over the Model Context Protocol.
Which orders produce events
Litmus sends webhook events only for order requests created through your integration, once your Litmus contact has registered your endpoint URL and signing secret.
An order request a user creates in Litmus itself, or through another integration, never produces an event for you. Events start with the order request's submission; a draft produces none. If your endpoint is registered after an order request was submitted, you receive events from the next change on.
Event types
Every delivery is a JSON object with type, timestamp (UTC, to the second) and data. Payloads never carry the order's content: no brief, assays or prices. Call get_order_request with order_request_id, or send the user to url, for details.
Sent each time an order request enters a new stage, starting with in_review when it is submitted. previous_stage is the stage it left, or null on submission.
JSON — order_request.stage_changedShow codeHide code
{
"type": "order_request.stage_changed",
"timestamp": "2026-10-21T12:00:00Z",
"data": {
"order_request_id": "7d1a6b2e-3c4f-4a5b-9e8d-1f2a3b4c5d6e",
"stage": "sourcing_quotes",
"previous_stage": "in_review",
"action_required": false,
"message": "Litmus is asking labs that can run this order for quotes.",
"url": "https://platform.litmus.science/order-requests/7d1a6b2e-3c4f-4a5b-9e8d-1f2a3b4c5d6e"
}
}Sent once per order request, when the first lab quote comes in. The stage does not change, so previous_stage is always null; stage is the order's current stage.
JSON — order_request.quote_receivedShow codeHide code
{
"type": "order_request.quote_received",
"timestamp": "2026-10-21T12:00:00Z",
"data": {
"order_request_id": "7d1a6b2e-3c4f-4a5b-9e8d-1f2a3b4c5d6e",
"stage": "sourcing_quotes",
"previous_stage": null,
"action_required": true,
"message": "Quotes are coming in for this order. Sign in to compare labs and pick one to move forward with.",
"url": "https://platform.litmus.science/order-requests/7d1a6b2e-3c4f-4a5b-9e8d-1f2a3b4c5d6e"
}
}Sent by hand by your Litmus contact to check that your endpoint receives and verifies a delivery. It carries no order request; data is a fixed marker.
JSON — integration.testShow codeHide code
{
"type": "integration.test",
"timestamp": "2026-10-21T12:00:00Z",
"data": {
"source": "Litmus",
"test": true
}
}The data fields of both order_request.* events:
stringThe order request's id, as returned by create_order_request.
stringThe order request's stage now. One of the stages below.
string | nullThe stage the order request left, or null when the event isn't a stage change or the order had no stage yet.
booleantrue when the user has to do something in Litmus for the order to move on; false when the event is informational.
stringOne sentence describing the event, written for the user. Safe to show as is.
stringThe order request's page in Litmus, where the user acts on it.
Stages and action_required
A submitted order request moves through these stages in order, each announced by an order_request.stage_changed event. action_required is true when the user has to act in Litmus for the order to move on.
action_required: falseSubmitted. Litmus is reviewing the order before asking labs for quotes.
action_required: falseLitmus is asking labs that can run the order for quotes.
action_required: trueA lab has been selected. The user signs in to pay and sign the standard agreements so work can start.
action_required: falseThe order is confirmed and the lab has started work.
order_request.quote_received normally arrives while the order is sourcing_quotes (a lab can still quote until the order is paid, so stage reports the order's stage at the time) and sets action_required to true: the user signs in to compare labs and pick one. The order request object has a later results stage; no event is sent for it yet.
Verifying a delivery
Every delivery is signed to the Standard Webhooks specification with the secret you gave Litmus. Verify it before acting on the body.
msg_…The event's id. It is the same on every delivery attempt of that event, so it is the key to dedupe on.
Unix secondsWhen this attempt was sent. A retry carries a fresh timestamp and signature, so verification with the usual five-minute tolerance passes for retries too.
v1,…HMAC-SHA256 over the id, the timestamp and the raw body, as a Standard Webhooks signature. Verify against the request body bytes as received, not a re-serialised copy.
whsec_…A Standard Webhooks secret: whsec_ followed by base64. You generate it and give it to your Litmus contact; Litmus stores it encrypted. Give a new one to rotate.
Use a Standard Webhooks library for your stack rather than checking the signature by hand. Reply with any 2xx status once you have stored the event; the response body is ignored.
Python — standardwebhooksShow codeHide code
from fastapi import FastAPI, Request, Response
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = FastAPI()
# The whsec_... secret you gave Litmus. Your own config, never in client code.
webhook = Webhook(LITMUS_WEBHOOK_SECRET)
@app.post("/litmus/webhooks")
async def litmus_webhook(request: Request) -> Response:
headers = {
name: request.headers[name]
for name in ("webhook-id", "webhook-timestamp", "webhook-signature")
}
try:
# Verifies the signature over the raw body and the timestamp's freshness,
# then returns the parsed body: {"type", "timestamp", "data"}.
event = webhook.verify(await request.body(), headers)
except WebhookVerificationError:
return Response(status_code=401)
# Litmus may deliver an event more than once; the id is the same each time.
if await already_handled(headers["webhook-id"]):
return Response(status_code=200)
await handle_litmus_event(event) # keep this quick; reply within 10 s
return Response(status_code=200)Retries and ordering
Delivery is at least once. Dedupe on webhook-id.
Any 2xx response marks the event delivered. The first attempt is made within seconds of the change.
Those statuses, a connection error, or no response within 10 seconds are retried with the same webhook-id and body. The wait doubles from 30 seconds per attempt, capped at one hour, with jitter (each wait is between half and the full step).
A 3xx or another 4xx is treated as a final answer: the event is not sent again. Redirects are not followed.
Litmus stops retrying an event 24 hours after it was recorded. Missed events are not replayed; get_order_request shows the order's current state.
Events are ordered per order request: an event is not sent while an older event for the same order request is still awaiting delivery, so a retrying event holds back the ones after it for that order until it is delivered or passes the horizon. Events for different order requests are independent. A crash between a delivery and its acknowledgement can send an event again under the same webhook-id, which is why delivery is at least once.
Your endpoint URL
Give your Litmus contact an https URL and the signing secret. Both can be changed later; sending a new secret replaces the old one.
The URL is stored in normalised form: the host is lowercased, a default port is dropped and a bare host gets a trailing slash, so https://Example.com is stored and called as https://example.com/. Paths and query strings are kept as given. Only https is accepted. Events are only recorded while both the URL and the secret are set; see which orders produce events.