Instead of asking the API every minute whether something changed, let MythicDeals tell you. When an event happens in your store we send a signed POST with a JSON body to your HTTPS endpoint, and retry for about 20 h 36 min if your server is down.
Set up an endpoint
- Add the URL. On Settings → Webhooks, or with
POST /webhooks(scopewebhooks:write). Pick the events you want. You can have up to 10 endpoints. - Store the secret. The signing secret (
whsec_…) is shown only once, when the endpoint is created or its secret is rotated. Keep it on your server only. - Send a test. “Send test” (or
POST /webhooks/{webhook}/test) delivers a signedpingright away and shows your server’s answer.
Events
Endpoints belong to a seller and only receive events about that seller’s store: their orders, the buyer messages in those orders, disputes on them and their listings. Buyers never receive webhooks. Every event below is sent once per subscribed endpoint.
| Event | When it fires | Who receives it |
|---|---|---|
| order.created | A buyer placed an order for one of your listings. It is not paid yet — do not deliver. | The seller of the order / listing |
| order.paid | Payment is confirmed and held in escrow. Deliver before delivery_due_at. | The seller of the order / listing |
| order.processing | The order was marked as in progress (by you, on the website or via the API). | The seller of the order / listing |
| order.delivered | The order was delivered — manually, via the API or by instant delivery. | The seller of the order / listing |
| order.completed | The buyer confirmed, or the order completed automatically at auto_release_at. | The seller of the order / listing |
| order.disputed | The buyer opened a dispute or a warranty claim. The money is frozen. | The seller of the order / listing |
| order.refunded | The order was refunded to the buyer. | The seller of the order / listing |
| order.cancelled | The order was cancelled. | The seller of the order / listing |
| order.expired | The buyer did not pay in time; the order expired and the stock is released. | The seller of the order / listing |
| message.created | The buyer sent a message in an order chat. Your own messages and system messages are not sent. | The seller of the order / listing |
| dispute.created | A dispute or a warranty claim was opened on one of your orders (usually by the buyer). Sent once per dispute, followed by a dispute.updated for the same change. Fetch the case with GET /disputes/{id}. | The seller of the order / listing |
| dispute.updated | A dispute on one of your orders was opened or changed status. One event per dispute change. | The seller of the order / listing |
| listing.sold_out | An order took the last unit of stock of a listing; it is now sold_out. | The seller of the order / listing |
| ping | Sent only by the “Send test” button or POST /webhooks/{webhook}/test. Always delivered, whatever events you chose. | The endpoint being tested |
All order.* events carry the full order as the seller sees it (same fields as GET /orders/{order}) plus previous_status (except order.created).
dispute.created fires once, when a dispute or a warranty claim is opened on one of your orders; dispute.updated fires for that same change and for every later status change (your counter-offer, an escalation, the resolution). Both carry the same data: the dispute in exactly the shape GET /disputes returns (requested_outcome, requested_amount_cents only for partial_refund, awaiting, pending_offer…) and a short order. Read the full case — offers, evidence, timeline — with GET /disputes/{id} (scope disputes:read) and answer with the dispute endpoints. Subscribe to dispute.created alone if you only need to know when a new case needs you.
Example payloads:
order.created
{
"id": "evt_01m5weqw0gxp9kz6h3hxcjf76s",
"type": "order.created",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "pending_payment",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": null,
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": null,
"delivery_due_at": null,
"delivered_at": null,
"auto_release_at": null,
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
}
}
}
order.paid
{
"id": "evt_01m5weqw10bhte2s9tvxk5kqzf",
"type": "order.paid",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "paid",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": null,
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": null,
"auto_release_at": null,
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "pending_payment"
}
}
order.processing
{
"id": "evt_01m5wf3fae3f86132407a1e735",
"type": "order.processing",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "processing",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": null,
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": null,
"auto_release_at": null,
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "paid"
}
}
order.delivered
{
"id": "evt_01m5wfc4aa3a27c01f65f1a8ad",
"type": "order.delivered",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "delivered",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": {
"delivered_at": "2026-10-26T16:02:10Z",
"has_credentials": true,
"proof_count": 0
},
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": "2026-10-26T16:02:10Z",
"auto_release_at": "2026-10-29T16:02:10Z",
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "processing"
}
}
order.completed
{
"id": "evt_01m5wf9dad7c7bae197a7b4b59",
"type": "order.completed",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "completed",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": {
"delivered_at": "2026-10-26T16:02:10Z",
"has_credentials": true,
"proof_count": 0
},
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": "2026-10-26T16:02:10Z",
"auto_release_at": "2026-10-29T16:02:10Z",
"completed_at": "2026-10-27T09:15:00Z",
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "delivered"
}
}
order.disputed
{
"id": "evt_01m5wfbfdbbc287846bfda92a7",
"type": "order.disputed",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "disputed",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": {
"delivered_at": "2026-10-26T16:02:10Z",
"has_credentials": true,
"proof_count": 0
},
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": "2026-10-26T16:02:10Z",
"auto_release_at": "2026-10-29T16:02:10Z",
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "delivered"
}
}
order.refunded
{
"id": "evt_01m5wf91cac38c8cc6e9aa140a",
"type": "order.refunded",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "refunded",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": {
"delivered_at": "2026-10-26T16:02:10Z",
"has_credentials": true,
"proof_count": 0
},
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": "2026-10-26T16:02:10Z",
"auto_release_at": "2026-10-29T16:02:10Z",
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "disputed"
}
}
order.cancelled
{
"id": "evt_01m5wfdf1427b62eba3b29b55a",
"type": "order.cancelled",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "cancelled",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": null,
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": null,
"auto_release_at": null,
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "paid"
}
}
order.expired
{
"id": "evt_01m5wf0ed2ee2e155ad487b276",
"type": "order.expired",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "expired",
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"game": "Valorant"
},
"quantity": 1,
"unit_price_cents": 9999,
"subtotal_cents": 9999,
"buyer_fee_cents": 0,
"discount_cents": 0,
"total_paid_cents": 9999,
"platform_fee_cents": 999,
"fee_percent": 10,
"seller_amount_cents": 9000,
"currency": "USD",
"buyer": {
"id": 2,
"name": "Rina"
},
"buyer_note": null,
"topup_fields": null,
"delivery": null,
"dispute": null,
"payment_due_at": "2026-10-26T16:54:50Z",
"paid_at": "2026-10-26T15:54:50Z",
"delivery_due_at": "2026-10-26T16:24:50Z",
"delivered_at": null,
"auto_release_at": null,
"completed_at": null,
"warranty_ends_at": null,
"created_at": "2026-10-26T15:54:50Z",
"updated_at": "2026-10-26T15:54:50Z"
},
"previous_status": "pending_payment"
}
}
message.created
{
"id": "evt_01m5wf2a8c4k1x9t3hq7r0vz6n",
"type": "message.created",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"order": {
"id": 2,
"number": "48292852",
"status": "processing"
},
"message": {
"id": 3,
"order_id": 2,
"sender": {
"id": 2,
"name": "Rina",
"role": "buyer"
},
"body": "Hi! Is the account ready?",
"is_system": false,
"attachments": [],
"read_at": null,
"created_at": "2026-10-26T15:54:50Z"
}
}
}
dispute.created
{
"id": "evt_01m5xsx3ng8f7xxrrennrv9dtd",
"type": "dispute.created",
"created_at": "2026-10-27T12:26:56Z",
"data": {
"dispute": {
"id": 1,
"order": {
"id": 3,
"number": "48296203",
"status": "disputed",
"listing_title": "Immortal 2 account, 42 skins",
"total_paid_cents": 11999
},
"status": "awaiting_seller",
"awaiting": "seller",
"opener_role": "buyer",
"category": "not_as_described",
"category_label": "Not as described",
"reason": "The rank is Ascendant 3, not Immortal 2 as the listing said.",
"requested_outcome": "partial_refund",
"requested_amount_cents": 5000,
"pending_offer": null,
"resolution": null,
"resolved_amount_cents": null,
"resolution_note": null,
"currency": "USD",
"escalated_at": null,
"resolved_at": null,
"created_at": "2026-10-27T12:26:56Z",
"updated_at": "2026-10-27T12:26:56Z",
"offer_count": 0
},
"order": {
"id": 3,
"number": "48296203",
"status": "disputed"
}
}
}
dispute.updated
{
"id": "evt_01m5wf3q1n7d2b5y8kx4c9t0re",
"type": "dispute.updated",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"dispute": {
"id": 1,
"order": {
"id": 3,
"number": "48296203",
"status": "disputed",
"listing_title": "Immortal 2 account, 42 skins",
"total_paid_cents": 11999
},
"status": "escalated",
"awaiting": "admin",
"opener_role": "buyer",
"category": "not_as_described",
"category_label": "Not as described",
"reason": "The rank is Ascendant 3, not Immortal 2 as the listing said.",
"requested_outcome": "partial_refund",
"requested_amount_cents": 5000,
"pending_offer": null,
"resolution": null,
"resolved_amount_cents": null,
"resolution_note": null,
"currency": "USD",
"escalated_at": "2026-10-27T13:05:12Z",
"resolved_at": null,
"created_at": "2026-10-27T12:26:56Z",
"updated_at": "2026-10-27T13:05:12Z",
"offer_count": 0
},
"order": {
"id": 3,
"number": "48296203",
"status": "disputed"
}
}
}
listing.sold_out
{
"id": "evt_01m5wf4d6p2s8z1w5ja3n7k9qb",
"type": "listing.sold_out",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"listing": {
"id": 1,
"title": "Immortal 2 account, 42 skins",
"type": "account",
"status": "sold_out",
"stock": 0,
"price_cents": 9999,
"currency": "USD"
},
"order": {
"id": 2,
"number": "48292852"
}
}
}
ping
{
"id": "evt_01m5weqvzcma48d9m8tfm3bjv9",
"type": "ping",
"created_at": "2026-10-26T15:54:50Z",
"data": {
"message": "Webhook test from MythicDeals.",
"endpoint_id": 1
}
}
The request
Each delivery is an HTTP POST with these headers and a JSON envelope. data depends on the event.
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | MythicDeals-Webhooks/1.0 |
| MythicDeals-Event | The event type, e.g. order.paid. |
| MythicDeals-Delivery | Unique id of this delivery (dlv_…). It stays the same on every retry — use it to drop duplicates. |
| MythicDeals-Signature | t=<unix time>,v1=<signature> — see below. |
{
"id": "evt_01m5weqw10bhte2s9tvxk5kqzf",
"type": "order.paid",
"created_at": "2026-10-26T15:54:50Z",
"data": { … }
}
id identifies the event: if the same event goes to two of your endpoints, both get the same id but different MythicDeals-Delivery values.
Verify the signature
Anyone can send a request to your URL, so check that it really comes from MythicDeals before trusting it. The signature is an HMAC-SHA256, in hex, of the timestamp, a dot and the raw request body, using your endpoint secret as the key:
signed_payload = t + "." + raw_body
v1 = hex(HMAC_SHA256(key = endpoint_secret, message = signed_payload))
- Split the
MythicDeals-Signatureheader on,and taketand everyv1. - Reject the request if
tis more than 5 minutes away from your clock (stops replays). - Compute the HMAC over the bytes exactly as received — before any JSON parsing — and compare it with
v1using a constant-time comparison.
use Illuminate\Http\Request;
// routes/web.php or routes/api.php -- exclude this URL from CSRF.
Route::post('/webhooks/mythicdeals', function (Request $request) {
$payload = $request->getContent(); // raw body, unparsed
$header = (string) $request->header('MythicDeals-Signature');
$secret = config('services.mythicdeals.webhook_secret'); // whsec_...
abort_unless(mythicdeals_verify($header, $payload, $secret), 400, 'Bad signature');
$delivery = $request->header('MythicDeals-Delivery');
if (cache()->add("mythicdeals:{$delivery}", true, now()->addDays(3))) {
ProcessMythicDealsEvent::dispatch(json_decode($payload, true)); // queue it
}
return response()->noContent(); // 2xx fast
});
function mythicdeals_verify(string $header, string $payload, string $secret, int $tolerance = 300): bool
{
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
if ($k === 't' && ctype_digit($v)) $t = (int) $v;
if ($k === 'v1') $signatures[] = $v;
}
if ($t === null || ! $signatures || abs(time() - $t) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) return true;
}
return false;
}
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.MYTHICDEALS_WEBHOOK_SECRET; // whsec_...
export function verify(header, rawBody, secret, tolerance = 300) {
let t = null;
const signatures = [];
for (const part of String(header).split(',')) {
const i = part.indexOf('=');
const k = part.slice(0, i).trim();
const v = part.slice(i + 1).trim();
if (k === 't' && /^\d+$/.test(v)) t = Number(v);
if (k === 'v1') signatures.push(v);
}
if (t === null || !signatures.length || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
return signatures.some((s) => s.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
// express.raw keeps the exact bytes -- do not use express.json() on this route.
app.post('/webhooks/mythicdeals', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.get('MythicDeals-Signature'), req.body, SECRET)) return res.sendStatus(400);
res.sendStatus(204); // answer first ...
const event = JSON.parse(req.body.toString('utf8'));
queue.add(req.get('MythicDeals-Delivery'), event); // ... then work (dedupe by delivery id)
});
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["MYTHICDEALS_WEBHOOK_SECRET"] # whsec_...
def verify(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
t, signatures = None, []
for part in header.split(","):
k, _, v = part.strip().partition("=")
if k == "t" and v.isdigit():
t = int(v)
elif k == "v1":
signatures.append(v)
if t is None or not signatures or abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in signatures)
@app.post("/webhooks/mythicdeals")
def mythicdeals():
# request.get_data() = the raw bytes; verify BEFORE parsing JSON.
if not verify(request.headers.get("MythicDeals-Signature", ""), request.get_data(), SECRET):
abort(400)
event = request.get_json()
enqueue(request.headers["MythicDeals-Delivery"], event) # dedupe + process later
return "", 204
Test your code with this known-good example (ignore the 5-minute window for this test):
| secret | whsec_test_8bQz1mN4vK7pR2xW9cY5tH3jL6sD0fGa |
| body | {"id":"evt_01m5weqvzcma48d9m8tfm3bjv9","type":"ping","created_at":"2026-10-26T15:54:50Z","data":{"message":"Webhook test from MythicDeals.","endpoint_id":1}} |
| MythicDeals-Signature | t=1792857290,v1=55812847f0e6020cc7a8cb03d70c8cd855e3050851d090bd796cdb10afba68ec |
Responses, retries and auto-disable
- Answer with any 2xx status within 10 seconds (5 seconds to connect). Anything else — 3xx, 4xx, 5xx, a timeout or a network error — counts as a failure. Redirects are not followed.
- A failed delivery is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h: 7 attempts in total over about 20 h 36 min. After the last attempt the delivery is marked
failed. - You can see every delivery, its payload, your response code and the next attempt on the Webhooks page or with
GET /webhooks/{webhook}/deliveries, and retry a failed one immediately (POST …/deliveries/{delivery}/retry). - After 20 failed attempts in a row the endpoint is turned off and we email you. Fix your server, then turn it back on — this resets the failure count. A single success also resets it.
pingtest events are sent once, never retried, and never count as failures.
URL rules (SSRF protection)
To protect our network and yours, endpoint URLs must:
- use
https://and be at most 500 characters; - not contain a username or password (
https://user:pass@…); - resolve only to public IP addresses. Private, loopback, link-local, reserved and carrier-grade NAT ranges (e.g.
10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,192.168.0.0/16,100.64.0.0/10,::1,fc00::/7,fe80::/10) are refused.
The address is checked when you save the endpoint and again before every delivery, and the connection is pinned to the IP that was checked, so a DNS change cannot point a delivery at an internal address.
Best practices
- Verify the raw body. Frameworks that parse JSON first may change spacing or key order and break the signature. Read the raw bytes, verify, then parse.
- Answer fast, work later. Return
2xxas soon as the signature checks out, and do the real work (delivering, syncing stock) in a queue. Slow answers are timeouts, and timeouts are retried. - Drop duplicates. Retries and network hiccups mean you may get the same delivery more than once. Store each
MythicDeals-Deliveryyou processed and skip repeats. - Do not rely on order. Events can arrive out of order (a retry of
order.paidmay arrive afterorder.delivered). Useprevious_status, compareupdated_at, or fetch the current state withGET /orders/{order}before acting. - Keep the secret secret. If it leaks, rotate it (Webhooks page or
PATCH /webhooks/{webhook}withrotate_secret). The old secret stops working at once; deliveries that fail meanwhile are retried, so update your server quickly. - Subscribe only to what you use, and use one endpoint per system so a slow tool does not get the others disabled.