Developer platform
Partner API & webhooks
For CRM, analytics and growth partners: read programmes and campaigns, pull or receive platform events, check a user's status, grant rewards and push ad spend — every request HMAC-signed, every webhook signed. Keys are issued by Midas; ask your account manager or contact us. For trading on your own account, use the account API instead.
Base URL: https://api.midas.exchange/api/v1/integrations/v1
Overview
A key carries only the scopes it was issued with. GET /me works with any valid key and shows yours.
| Scope | Grants |
|---|---|
rewards:read | Programmes, the rewards calendar and campaigns |
events:read | Pull the event feed (GET /events) |
webhooks:manage | Create and manage your own webhook endpoints |
users:read | One user's status: KYC, country once verified, acquisition channel. Never email, phone, balance or IP |
ads:write | Push daily ad spend (POST /ads/spend) |
rewards:grant | Grant fee credits or points, within your daily cap and the programme budget |
Authentication
Every request except GET /openapi.yaml carries four headers:
| Header | Value |
|---|---|
X-MIDAS-KEY | Your key id (mdk_…) |
X-MIDAS-TIMESTAMP | Unix seconds; within ±30 s of the server's clock |
X-MIDAS-SIGNATURE | Hex HMAC-SHA256(secret, signing string) |
X-MIDAS-IDEMPOTENCY-KEY | Required on POST, honoured on PATCH / DELETE; 1–128 of A–Z a–z 0–9 . _ : - |
The signing string is five lines joined by \n, with no trailing newline. Unlike the account API, the query string is signed.
<timestamp>
<METHOD upper case>
<path as sent, e.g. /api/v1/integrations/v1/events>
<canonical query: decode, sort by name then value, RFC 3986 encode (space = %20), join name=value with &>
<hex sha256 of the raw body; e3b0c442…b855 for an empty body>Worked example (the secret is an example only). With TS=1791072000 the two signatures are ee463493fc90db2b162c782366f71d9601b07bfad5f643b5a191226b472c4b1c and a92b23ffeed9f2b9b3804264e13ac02992cc96d5de62ba17a341858d9115f542.
KEY=mdk_5f1c2a9e8b7d6c4a3f2e1d0c
SECRET=mdsk_EXAMPLE_ONLY_q7Xv2Lr9TzK4mWb8Np3Hs6Df1Gj5Yc0A
HOST=https://api.midas.exchange
TS=$(date +%s)
# GET with a query — write the query already canonical (sorted, RFC 3986).
P=/api/v1/integrations/v1/events
Q='after=1200&limit=2&types=deposit.confirmed%2Cuser.kyc_verified'
BODY_SHA=$(printf '%s' '' | openssl dgst -sha256 -hex | awk '{print $NF}')
SIG=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" GET "$P" "$Q" "$BODY_SHA" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -sS "$HOST$P?$Q" -H "X-MIDAS-KEY: $KEY" -H "X-MIDAS-TIMESTAMP: $TS" -H "X-MIDAS-SIGNATURE: $SIG"
# POST — sign the exact bytes you send.
P=/api/v1/integrations/v1/rewards/grants
BODY='{"user_id":"9e2b7d14-0c6a-4b38-8f5e-1a4d6c0b3e92","type":"fee_credit","amount":"2","ttl_days":30,"reference":"promo-2026-10-0001"}'
BODY_SHA=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIG=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST "$P" "" "$BODY_SHA" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -sS -X POST "$HOST$P" -H 'Content-Type: application/json' -d "$BODY" \
-H "X-MIDAS-KEY: $KEY" -H "X-MIDAS-TIMESTAMP: $TS" -H "X-MIDAS-SIGNATURE: $SIG" \
-H "X-MIDAS-IDEMPOTENCY-KEY: grant-promo-2026-10-0001"After a secret rotation the previous secret keeps working for the overlap Midas chose (24 hours by default).
Idempotency & limits
- The first response to an idempotency key (any 2xx / 4xx) is kept 24 hours. The same key and request returns it again with
X-Midas-Idempotent-Replay: true; the same key with a different request is409 idempotency_key_reused; a 5xx releases the key. - Rate limit: a token bucket per client,
X-RateLimit-Limit/X-RateLimit-Remainingon every response,429 rate_limitedwithRetry-After. - Body ≤ 1 MB ·
limiton /events ≤ 500 · 10 webhook endpoints per client · a range replay ≤ 10,000 events · one fee credit ≤ 1,000 USD · points ≤ 100,000,000. - Amounts are decimal strings; times are RFC 3339 UTC.
Endpoints
| Method | Path | Scope |
|---|---|---|
| GET | /openapi.yaml | public |
| GET | /me | any key |
| GET | /programmes | rewards:read |
| GET | /calendar | rewards:read |
| GET | /campaigns | rewards:read |
| GET | /events?after=&types=&limit= | events:read |
| GET · POST | /webhooks | webhooks:manage |
| GET · PATCH · DELETE | /webhooks/:id | webhooks:manage |
| POST | /webhooks/:id/rotate-secret | webhooks:manage |
| POST | /webhooks/:id/test | webhooks:manage |
| POST | /webhooks/:id/replay | webhooks:manage |
| GET | /webhooks/:id/deliveries | webhooks:manage |
| GET | /webhooks/:id/deliveries/:delivery_id | webhooks:manage |
| POST | /webhooks/:id/deliveries/:delivery_id/replay | webhooks:manage |
| GET | /users/:id | users:read |
| POST | /rewards/grants | rewards:grant |
| GET | /rewards/grants/:reference | rewards:grant |
| POST | /ads/spend | ads:write |
Pull the feed with a cursor:
{"success":true,"data":{"events":[
{"id":1802,"type":"user.kyc_verified","user_id":"9e2b7d14-…","occurred_at":"2026-10-04T09:05:00Z","data":{"kyc_level":1}},
{"id":1834,"type":"deposit.confirmed","user_id":"9e2b7d14-…","occurred_at":"2026-10-04T09:20:00Z",
"data":{"asset":"USDT","amount":"250","amount_usd":"250","wallet_type":"spot"}}],
"next_after":1834,"has_more":true}}
// Keep next_after even from an empty page: it advances to the settled horizon.Reward grants
A fee credit is paid from the Midas rewards pool under the partner_api programme and its budget; nothing is minted. The user must be KYC-verified and not on a reward hold. Caps apply per client per day, per user per day across all partners, and to the programme. A refusal moves nothing.
POST /rewards/grants
{"user_id":"9e2b7d14-…","type":"fee_credit","amount":"2","ttl_days":30,"reference":"promo-2026-10-0001"}
201 {"success":true,"data":{"replayed":false,"grant":{"id":"6b1f0e2a-…","reference":"promo-2026-10-0001",
"user_id":"9e2b7d14-…","type":"fee_credit","amount":"2","granted_amount":"2","usd_value":"2","ttl_days":30,
"expires_at":"2026-11-03T09:20:00Z","program_code":"partner_api","created_at":"2026-10-04T09:20:00Z"}}}
// points: {"type":"points","amount":500,…} → usd_value "0.5" at the default rate (1,000 points = 1 USD)The same reference with the same body returns the first grant (replayed: true); with a different body it is 409 reference_conflict.
Webhooks
Midas POSTs each event to your https endpoint and signs it:
POST <your url>
Content-Type: application/json
User-Agent: Midas-Webhooks/1
X-Midas-Event-Id: 1834
X-Midas-Event-Type: deposit.confirmed
X-Midas-Delivery-Id: 5c2e…
X-Midas-Attempt: 1
X-Midas-Signature: t=1791105600,v1=98f791c88dfa79554362f36881b6dcc2f264bda8af28466bf4fc89bdf53d1d79
{"id":1834,"type":"deposit.confirmed","occurred_at":"2026-10-04T09:20:00Z","user_id":"9e2b7d14-0c6a-4b38-8f5e-1a4d6c0b3e92","data":{"amount":"250","amount_usd":"250","asset":"USDT","wallet_type":"spot"}}- Verify
v1 = HMAC-SHA256(secret, "<t>.<raw body>")over the raw bytes, and reject atmore than 5 minutes old. During a secret rotation a secondv1is signed with the previous secret. - 2xx is success. A 4xx other than 408 / 409 / 425 / 429 fails at once; anything else is retried after 1 s, 5 s, 30 s, 5 min, 30 min, 2 h, 6 h and 24 h.
- Delivery is at least once: de-duplicate on
X-Midas-Event-Id. 50 failures in a row disable the endpoint. - https only, on a public host: no credentials in the URL, no internal or private addresses (checked again on every attempt, after DNS). No redirects are followed; 10 s timeout.
Node.js:
const crypto = require("crypto");
// Express: app.post("/midas", express.raw({ type: "application/json" }), handler)
function verifyMidasSignature(header, rawBody, secret, toleranceSec = 300) {
const fields = header.split(",").map((p) => p.trim().split("="));
const t = (fields.find(([k]) => k === "t") || [])[1];
const sigs = fields.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || sigs.length === 0 || Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(t + ".").update(rawBody).digest();
return sigs.some((s) => { const b = Buffer.from(s, "hex"); return b.length === expected.length && crypto.timingSafeEqual(b, expected); });
}Python:
import hashlib, hmac, time
def verify_midas_signature(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
fields = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in fields if k == "t"), None)
sigs = [v for k, v in fields if k == "v1"]
if t is None or not sigs or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s.lower()) for s in sigs)Test vector: secret whsec_EXAMPLE_ONLY_Rk3Vn8Qw1Zt6Yb4Mc9Lp2Hx7Js5Fd0Ga with the request above gives the signature shown in its header.
Event types
Never an email, phone or IP. Types without a user have user_id: null.
| Type | data |
|---|---|
user.signed_up | {"method":"email","platform":"web","referred":false} |
user.kyc_verified | {"kyc_level":1} |
deposit.confirmed | {"asset":"USDT","amount":"250","amount_usd":"250","wallet_type":"spot"} |
withdrawal.completed | {"withdrawal_id":"0b9c…","asset":"USDT","network":"bsc","amount":"100","fee":"1","amount_usd":"100","tx_hash":"0x5d1c…","completed_at":"…"} |
trade.first | {"market":"spot","symbol":"BTC/USDT","notional_usd":"152.4","organic":true} |
reward.quest_completed | {"quest_code":"welcome_deposit_100","program_code":"welcome_ladder","user_quest_id":"7d3e…","completed_at":"…"} |
reward.credited | {"source":"partner_api","reference":"promo-2026-10-0001","program_code":"partner_api","reward_type":"fee_credit","amount":"2","asset":"USDT","expires_at":"…"} |
reward.campaign_started / _ended | {"campaign_code":"weekly_trading_competition","kind":"competition","run_id":"c1f6…","starts_at":"…","ends_at":"…"} |
referral.attached | {"referrer_user_id":"4a8e…","referral_code":"MIDAS8K2","sub_id":"yt-oct"} |
referral.commission_credited | {"referee_user_id":"9e2b…","source":"spot","asset":"USDT","amount":"0.42","amount_usd":"0.42","rate_pct":"20"} |
affiliate.tier_changed | {"from_tier":"partner","to_tier":"bronze","commission_rate_pct":"25","source":"auto"} |
lucky_draw.won | {"draw_code":"daily_spin","spin_id":"2c5f…","prize_type":"fee_credit","amount":"0.5","asset":"USDT"} |
reward_shop.purchased | {"item_code":"fee_credit_1","order_id":"5f0a…","cost_points":1000,"item_type":"fee_credit","amount":"1"} |
airdrop.claimed | {"campaign_id":"d8b4…","asset":"MDS","amount":"100"} |
launchpool.staked | {"project_id":"a3d7…","mode":"launchpool","stake_asset":"USDT","amount":"500","user_total_staked":"500"} |
launchpool.rewards_claimed | {"project_id":"a3d7…","reward_asset":"USDT","amount":"1.25"} |
kickstarter.finished | {"project_id":"e6c2…","goal_met":true,"goal_amount":"100000","committed_amount":"125000","stake_asset":"USDT","reward_asset":"XYZ"} |
competition.prize_paid | {"competition_id":"b7e1…","rank":1,"asset":"USDT","amount":"60"} |
fee_promo.started / ended | {"promo_code":"zero_fee_btc","markets":"spot","symbols":["BTCUSDT"],…} |
social.task_verified | {"task_code":"join_telegram","provider":"telegram","action":"join"} |
webhook.test | {"endpoint_id":"3e9a…","message":"Test event from Midas"} |
Errors
A refusal is {"success": false, "message": "…", "code": "<code>", "details": {…}}. Branch on code.
| Status | Codes |
|---|---|
| 400 | invalid_json, invalid_body, idempotency_key_required, invalid_idempotency_key |
| 401 | missing_credentials, invalid_timestamp, timestamp_out_of_window, invalid_key, invalid_signature |
| 403 | client_disabled, ip_not_allowed, insufficient_scope, grants_not_allowed, programme_disabled |
| 404 | endpoint_not_found, delivery_not_found, user_not_found, grant_not_found |
| 409 | idempotency_in_progress, idempotency_key_reused, reference_conflict, endpoint_limit_reached, endpoint_disabled, delivery_in_flight, delivery_not_finished, test_ping_not_replayable |
| 413 | body_too_large |
| 422 | validation_failed, unknown_event_type, invalid_url, replay_too_large, kyc_required, reward_on_hold, points_unavailable |
| 429 | rate_limited, client_daily_cap_reached, user_daily_cap_reached, programme_daily_cap_reached |
| 503 | budget_exhausted, pool_insufficient, unavailable — nothing moved; retry later |