mila
FiyatlandırmaKılavuzlarMCPSDKHakkımızda
Giriş yapKaydol

Özellik kılavuzları

Webhook'lar

Posta ve hesap olayları için imzalı, yeniden denenen HTTP geri çağrıları alın, eşleşmeyen adreslere gelen postayı karşılayın ve her isteği doğrulayın.

Webhook'lar nedir

Webhook uç noktası, kuruluşunuzda bir şey olduğunda mila'nın çağırdığı HTTPS URL'nizdir: bir posta kutusu oluşturulur, gönderdiğiniz bir mesaj teslim edilir ya da geri döner, kodunuza yönlendirdiğiniz bir adrese mesaj gelir. Her olay önce kalıcı olarak kaydedilir, sonra eşleşen her uç noktaya imzalı bir POST olarak iletilir. Başarısız bir teslim kaybolmaz: mila beş gün boyunca yeniden dener, 30 gün saklar ve elle yeniden göndermenize izin verir.

Her uç noktanın bir URL'si, isteğe bağlı bir açıklaması, istediği olay türleri (ya da hepsi), isteğe bağlı olarak sınırlandığı alan adları (boş = her alan adı), bir gelen posta biçimi (raw ya da json, bkz. "Posta almak") ve bir etkin/pasif anahtarı vardır. Oluşturduğunuzda mila imza sırrını yalnızca bir kez gösterir -- o an kopyalayın; bkz. "İmzaları doğrulamak".

API

API anahtarlarının webhooks:read ve/veya webhooks:write yetkisi olmalı ve anahtarın sahibi organizasyonun sahibi ya da tüm alan adlarına erişen bir yöneticisi olmalıdır. Yetkilerden önce oluşturulmuş anahtarlarda bu yetkiler yoktur -- yeni bir anahtar oluşturun.

ÇağrıYetkiNe yapar
GET /api/v1/webhook-event-typeswebhooks:readOlay türleri ve gruplarının kataloğu
GET /api/v1/webhooks, GET /api/v1/webhooks/{id}webhooks:readUç noktaları (24 saatlik sağlıkla) listeler ya da birini okur
POST /api/v1/webhookswebhooks:writeOluşturur; yanıtta secret bir kez gelir
PATCH /api/v1/webhooks/{id}, DELETE /api/v1/webhooks/{id}webhooks:writeDeğiştirir ya da siler (silmek bekleyen teslimleri de düşürür)
POST /api/v1/webhooks/{id}/rotate-secretwebhooks:writeYeni sır; eski sır 24 saat daha imzalamaya devam eder
POST /api/v1/webhooks/{id}/testwebhooks:writeHemen bir webhook.test olayı gönderir ve sonucu bildirir
GET /api/v1/webhooks/{id}/deliveries, .../deliveries/{deliveryId}webhooks:readTeslim kaydı; status, eventType, createdAfter, createdBefore ile süzülür
POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/retrywebhooks:writeTek bir teslimi yeniden gönderir
POST /api/v1/webhooks/{id}/deliveries/retrywebhooks:writeSüzgeçle toplu yeniden gönderim (çağrı başına en çok 1000; hasMore yeniden çağırmanızı söyler)
GET /api/v1/domains/{domainId}/inbound-webhookdomains:read"Eşleşmeyen adresleri webhook'a ilet" ayarının açık olup olmadığı ({ "enabled": true })
PUT /api/v1/domains/{domainId}/inbound-webhookdomains:write (yönetici)"Eşleşmeyen adresleri webhook'a ilet" ayarını açar ya da kapatır ({ "enabled": true }). 409 CATCHALL_ACTIVE / DOMAIN_ALIAS_ACTIVE; posta sunucusu + alt adresleme değişikliğini uygulayamazsa 503 MAIL_SERVER_UNAVAILABLE (açarken hiçbir şey değişmemiştir, tekrarlayın; kapatırken ayar zaten kapalıdır ve + birkaç dakika içinde kendiliğinden geri gelir)

Uç nokta oluşturmak:

curl -X POST "https://api.mila.cx/api/v1/webhooks" \
  -H "Authorization: Bearer $MILA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/mila",
    "description": "Canlı olaylar",
    "eventTypes": ["email.sent", "email.delivered", "email.bounced", "mailbox.created"]
  }'

eventTypes, aşağıdaki katalogdan boş olmayan bir tür listesidir ya da tümü için tam olarak ["*"] ("*" başka türlerle birleştirilemez). webhook.test yalnızca test eylemiyle gönderilir ve abone olunamaz. Sınırlar: url en çok 2048 karakter, description en çok 500, domainIds en çok 500 öğe.

Olaylar

Her olay aynı zarfta bir JSON belgesidir:

{
  "id": "evt_01JBQ6Z9X4M2K7R8T5V3N1W0YC",
  "type": "mailbox.created",
  "createdAt": "2026-10-30T09:15:42.118Z",
  "organizationId": "<organizasyon kimliği>",
  "data": { "mailboxId": "...", "domainId": "...", "address": "sales@example.com" }
}

id, olayın uç noktalarınıza yapılan tüm teslimlerinde aynıdır. (JSON zarfının tek istisnası raw biçimindeki email.received'dir; bkz. "Posta almak".)

OlayNe zaman tetiklenirdata alanları
email.receivedBir mesaj gelir. Alan adındaki bir posta kutusu adresi için: yalnızca üst veri. "Eşleşmeyen adresleri webhook'a ilet" açık bir alan adında eşleşmeyen bir adres için: mesajın tamamı, bkz. "Posta almak"Üst veri: messageId, from, to, subject, size, format: "metadata". Eşleşmeyen, raw biçimi: JSON gövde yok (gövde mesajın kendisidir). Eşleşmeyen, json biçimi: messageId, from, subject, size, format: "raw", ayrıca raw (base64) ya da rawOmitted
email.sentAlan adlarınızdan birinin kimliği doğrulanmış bir gönderici (SMTP girişi, webmail, REST API, MCP) bir mesaj gönderdi. Mesaj başına bir olay, tüm alıcıları listelerqueueId, messageId, from, recipients[], subject (mila'nın kendisinin kaydetmesi gereken bir API gönderiminde queueId yerine submissionId bulunur)
email.deliveredUzak sunucu mesajı bir alıcı için kabul etti. Kendi yönlendirmenizin aldığı bir alıcı (takma ad, yeniden yazma, yönlendirme ya da "eşleşmeyen adresleri webhook'a ilet" açık bir alan adında eşleşmeyen bir adres) hemen 250 2.1.5 Accepted for local routing yanıtıyla teslim edildi sayılırqueueId, submissionId (API gönderimleri), messageId, recipient, smtpReply
email.delivery_delayedGeçici bir hata; mila hâlâ deniyor. Alıcı başına 6 saatte en çok bir kezemail.delivered ile aynı
email.bouncedBir alıcı için kalıcı hataemail.delivered ile aynı
email.delivery_unknownmila bir alıcıya ne olduğunu öğrenemedi: aşağıdaki garantilere bakınemail.delivered ile aynı
domain.createdOrganizasyona bir alan adı eklendidomainId, name
domain.verifiedBir alan adı etkin oldu. Alan adı etkin değilken etkin olduğu her seferinde tetiklenir; yalnızca ilk doğrulamada değil, devre dışı bırakılmış bir alan adının yeniden etkinleştirilmesinde dedomainId, name
domain.dns_issueMX, SPF ve DKIM'in üçü de sağlıklı olan etkin bir alan adı sağlıksız hale geldi. Bozulma başına bir kez tetiklenir; bozuk kaldığı sürece her kontrolde tetiklenmezdomainId, name, issues[] (record, expected, found)
domain.deletedBir alan adı organizasyondan kaldırıldıdomainId, name
mailbox.createdBir posta kutusu oluşturuldu (gizli sistem posta kutuları hiç olay üretmez)mailboxId, domainId, address
mailbox.updatedBir posta kutusu ayarı değişti; parola değişikliği ya da süre dolumunun kutuyu devre dışı bırakması dahilmailboxId, domainId, address, changes[] -- değişen alanların adları (may_send, active, password gibi), değerleri asla
mailbox.deletedBir posta kutusu silindi (özgün adresiyle)mailboxId, domainId, address
alias.created, alias.deletedBir takma ad eklendi ya da kaldırıldıaliasId, domainId, address, destinations[]
forwarding.created, forwarding.deletedBir yönlendirme kuralı eklendi ya da kaldırıldıforwardingId, mailboxAddress, destination
webhook.test"Test gönder"e bastınız (panel ya da API)message, endpointId

Bilmeye değer noktalar:

Neler garanti

"Kalıcı"nın burada ne demek olduğunu net söyleyelim:

OlayGaranti
email.receivedGarantili. mila mesajı kabul etmeden önce kaydedilir. mila kaydı yazamazsa gönderen sunucuya geçici bir SMTP hatası (4xx) döner ve gönderen yeniden dener. (Posta kutusu adresleri için üst veri olayları yalnızca uç noktalarınızdan biri o alan adı için email.received'e abone olduğu sürece kaydedilir; yeni oluşturulan ya da süzgeci değişen bir uç nokta bunların yaklaşık 30 saniyesini kaçırabilir. Eşleşmeyen adres postası her zaman kaydedilir.)
email.sentKimliği doğrulanmış gönderimler (587'de SMTP girişi, webmail, REST API, MCP) için, uç noktalarınızdan biri gönderenin alan adı için bir giden posta olayına (email.sent, email.delivered, email.delivery_delayed, email.bounced, email.delivery_unknown ya da tüm olaylar) abone olduğu sürece garantili. Mesaj kabul edilmeden önce kaydedilir; kaydedilemezse gönderime, kabul edilmiş ama kaydedilmemiş bir mesaj yerine geçici bir SMTP hatası döner. Hiçbir uç noktanın abone olmadığı sırada gönderilen mesajlar hiç izlenmez -- ne email.sent ne de sonradan sonuç olayları gelir -- ve bir uç nokta eklediğinizde geriye dönük doldurma yapılmaz; yeni oluşturulan ya da süzgeci değişen bir uç nokta gönderimlerin yaklaşık 30 saniyesini kaçırabilir.
email.delivered, email.delivery_delayed, email.bouncedNeredeyse garantili. Sonuç, bellekte tutulan posta sunucusu telemetrisinden gelir: yalnızca posta sunucusu yanlış anda yeniden başlarsa kaybolabilir. Bunun üzerine 5 dakikada bir çalışan bir süpürme, 10 dakikadan eski her mesaj için posta kuyruğuna bakar ve mümkünse sonucu kurtarır.
email.delivery_unknownGarantili ve açık. Bir alıcının mesajı kuyruktan çıkmış ve hiçbir sonuç görülmemişse mila yaklaşık 15 dakikalık bir bekleme süresinden sonra sessiz kalmak yerine size "bilinmiyor" der. Bu "başarısız oldu" demek değil, "bilmiyoruz" demektir: alıcı tarafına bakın.
domain.*, mailbox.*, alias.*, forwarding.*Garantili. Olay, değişikliğin kendisiyle aynı veritabanı işleminde yazılır: ya ikisi birden olur ya hiçbiri.

Teslim

mila en az bir kez teslim eder. Bir alıcı aynı teslimi birden fazla görebilir (yanıtınızı gönderdiğiniz hâlde zaman aşımına uğrayan bir yeniden deneme, teslim sırasında bir çökme) ve idempotent olmalıdır. Olaylar arasında sıra garantisi yoktur.

Her istek şu başlıklarla bir POST'tur:

BaşlıkAnlamı
X-Mila-EventOlay türü, örneğin mailbox.created
X-Mila-Event-IdOlayın kimliği (evt_...); onu alan tüm uç noktalarda aynı
X-Mila-Delivery-IdBu teslimin kimliği. Her yeniden denemede sabittir -- tekrarları bununla ayıklayın
X-Mila-Attemptİlk deneme için 1, sonra 2, 3, ... (elle yeniden gönderim saymaya devam eder)
X-Mila-TimestampBu denemenin imzalandığı Unix saniyesi
X-Mila-Signaturev1=<hex>; sır döndürme sırasında virgülle ayrılmış iki değer
X-Mila-RecipientYalnızca email.received'de: zarf alıcısı; düz ASCII ise geldiği gibi (rsvp+<token>@... gibi adresler için her zaman böyledir). ASCII olmayan karakterler içeren bir adres (örneğin şükrü@...) yüzde kodlanmış UTF-8 olarak gönderilir (%C5%9F%C3%BCkr%C3%BC%40..., JavaScript'in encodeURIComponent'inin ürettiği biçimde): böyle bir değer hiçbir zaman düz bir @ içermez, yani @ içermeyen bir başlık decodeURIComponent / urllib.parse.unquote ile çözülür
User-AgentMila-Webhooks/1.0

Gövde JSON zarfıdır (Content-Type: application/json); raw biçimindeki email.received için ise ham mesajdır.

Yanıtınız nasıl okunur

Yeniden denemeler bir önceki denemeden 1 dakika, 5 dakika, 15 dakika, 1 saat ve 3 saat sonra, ardından 6 saatte bir olur; her birinde %10 sapma (jitter) vardır ve ilk denemeden 5 gün sonrasına kadar sürer. Sonraki deneme bu pencerenin dışına düşecekse teslim failed olur ve mila operatörlerine uyarı gider (uyarı yalnızca kimlik ve sayı taşır; mesaj içeriği ya da alıcı asla).

Bir teslim şu durumlardan birinde biter:

DurumAnlamı
pending, deliveringSırada ya da şu an gönderiliyor
delivered2xx yanıtladınız
rejected4xx yanıtladınız (408/429 hariç)
failedBeş günlük yeniden deneme bitti (ya da duraklatılmış bir uç noktada 30 günden uzun tutuldu, ya da -- hata raw_message_purged -- bir email.received'in ham iletisi zaten silinmişti, aşağıya bakın)
too_largeGövde 1 MiB'ı aşacaktı; bkz. "Posta almak"
cancelledUç nokta, gönderilmeden önce olayla artık eşleşmeyecek şekilde düzenlendi

Biten teslimler, denemeleri ve olay gövdeleri 30 gün saklanır. Bu sürede herhangi birini (teslim edilmiş olanı bile) panelde teslim kaydından tek tek ya da süzgeçle toplu olarak yeniden gönderebilirsiniz (durum belirtmeyen bir toplu yeniden gönderim failed, rejected ve too_large olanları alır). Yeniden gönderim aynı X-Mila-Delivery-Id ile, yeni bir 5 günlük deneme penceresiyle ve devam eden deneme sayısıyla çıkar. webhook.test teslimleri yeniden gönderilemez -- yeni bir test gönderin. Ham email.received teslimleri yalnızca mila iletiyi hâlâ saklarken yeniden gönderilebilir: saklanan kopya, iletinin tüm teslimleri başarılı olur olmaz silinir (aksi hâlde sonuncusu bittikten 30 gün sonra ya da alan adıyla birlikte). Ondan sonra yeniden gönderim 409 RAW_MESSAGE_PURGED ile reddedilir; yine de gönderime ulaşan bir teslim (toplu yeniden gönderim) yeniden denenmeden hemen failed olarak, raw_message_purged hatasıyla biter.

Bir uç noktayı duraklatmak

Bir uç noktayı kapatmak onu duraklatır; olayları düşürmez. Eşleşen olaylar kaydedilmeye devam eder ve pending olarak bekler. Uç noktayı yeniden açtığınızda her biri yeni bir 5 günlük deneme penceresiyle gönderilir. Duraklatılmış bir uç noktada 30 günden uzun tutulan teslim failed olur (hata endpoint_paused_too_long) ve diğer başarısızlıklar gibi 30 gün daha saklanır. Bir uç noktayı silmek farklıdır: uç noktayı ve tüm teslimlerini, bekleyenler dahil, düşürür; bunlar bir daha gönderilmez. Olayları sonra isteyebilecekseniz silmek yerine duraklatın.

İmzaları doğrulamak

URL'nizi öğrenen herkes ona sahte bir istek gönderebilir; bu yüzden her isteği doğrulayın.

İmza, üç şeyden oluşan bir dizge üzerinde HMAC-SHA256'dır:

imzalanan dizge = X-Mila-Timestamp + "\n" + X-Mila-Recipient + "\n" + hex(sha256(ham gövde))
X-Mila-Signature = "v1=" + küçük harfli hex( HMAC-SHA256(anahtar, imzalanan dizge) )

Node.js:

import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;

export function verifyMilaWebhook({ secretHex, headers, rawBody, now = Date.now() / 1000 }) {
  const timestamp = headers['x-mila-timestamp'];
  const recipient = headers['x-mila-recipient'] ?? '';
  const signature = headers['x-mila-signature'] ?? '';

  // Eski ya da çok ileri tarihli zaman damgalarını reddet (tekrar koruması).
  if (!/^\d+$/.test(timestamp ?? '') || Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) return false;

  const bodyHash = createHash('sha256').update(rawBody).digest('hex');
  const expected = createHmac('sha256', Buffer.from(secretHex, 'hex'))
    .update(`${timestamp}\n${recipient}\n${bodyHash}`)
    .digest('hex');

  // Sır döndürme sırasında başlıkta iki değer olabilir: "v1=<yeni>,v1=<eski>".
  let ok = false;
  for (const part of signature.split(',')) {
    const candidate = part.trim();
    if (!candidate.startsWith('v1=')) continue;
    const got = Buffer.from(candidate.slice(3), 'utf8');
    const want = Buffer.from(expected, 'utf8');
    if (got.length === want.length && timingSafeEqual(got, want)) ok = true;
  }
  return ok;
}

Python:

import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_mila_webhook(secret_hex: str, headers: dict, raw_body: bytes, now: float | None = None) -> bool:
    now = time.time() if now is None else now
    timestamp = headers.get("x-mila-timestamp", "")
    recipient = headers.get("x-mila-recipient", "")
    signature = headers.get("x-mila-signature", "")

    # Eski ya da çok ileri tarihli zaman damgalarını reddet (tekrar koruması).
    if not timestamp.isdigit() or abs(now - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    body_hash = hashlib.sha256(raw_body).hexdigest()
    expected = hmac.new(
        bytes.fromhex(secret_hex),
        f"{timestamp}\n{recipient}\n{body_hash}".encode(),
        hashlib.sha256,
    ).hexdigest()

    # Sır döndürme sırasında başlıkta iki değer olabilir: "v1=<yeni>,v1=<eski>".
    ok = False
    for part in signature.split(","):
        part = part.strip()
        if part.startswith("v1=") and hmac.compare_digest(part[3:], expected):
            ok = True
    return ok

Uygulamanızı bu test vektörüyle sınayın. Sır 0000000000000000000000000000000000000000000000000000000000000000 (64 sıfır, yani 32 sıfır bayt), zaman damgası 1761800000, alıcı rsvp+abc.def@reply.vnnx.app ve gövde hello iken başlık şu olmalıdır:

X-Mila-Signature: v1=025d4e1d55076d9a8c7322e8ab58c2f80bebe174300ab0604b305dd071f31f9c

(Yukarıdaki iki örnek de bu girdiler için tam olarak bu değeri üretir.)

Sırrı döndürmek

Panelden ya da POST /api/v1/webhooks/{id}/rotate-secret ile döndürün. Yeni sır bir kez gösterilir. Sonraki 24 saat boyunca her teslim -- süren yeniden denemeler dahil -- ikisiyle birden imzalanır; böylece olay kaçırmadan yeni sırra geçebilirsiniz. Bu pencere içinde ikinci bir döndürme 409 ROTATION_IN_PROGRESS ile reddedilir, çünkü hâlâ geçiş yapmakta olduğunuz bir sırrı emekliye ayırırdı; geçersiz kılmak için { "force": true } gönderin -- bu, eski sırrın imzalamasını hemen durdurur.

Posta almak (email.received)

İki tür email.received vardır.

Posta kutusu teslimleri -- yalnızca üst veri. Uç noktanızın kapsadığı bir alan adındaki gerçek bir posta kutusuna mesaj gönderildiğinde gönderen, To, konu, Message-ID ve boyutu içeren bir JSON olayı alırsınız -- gövde yok, ek yok. Bu bir bildirimdir, postanın kopyası değil.

Eşleşmeyen adresler -- mesajın tamamı. Bir alan adında Eşleşmeyen adresleri webhook'a ilet ayarını açabilirsiniz (panel: alan adının ayarları; API: PUT /api/v1/domains/{domainId}/inbound-webhook, gövde { "enabled": true }). O alan adındaki posta kutusu, takma adı ya da kimliği olmayan her adrese gelen posta reddedilmek yerine kabul edilir ve eksiksiz mesajla email.received uç noktalarınıza iletilir. rsvp+<jeton>@reply.example.com gibi "e-postayla yanıtla" özellikleri böyle kurulur.

Uç noktanın inboundFormat değeri biçimi belirler:

Açarken bilmeniz gerekenler:

Ham postada Authentication-Results'a güvenmek

Kodunuz kimlik doğrulama sonuçlarına bakarak karar veriyorsa ("bu gerçekten o göndericiden mi geldi?") bu bölümü dikkatle okuyun. Bir gönderen, sahte bir Authentication-Results (A-R) başlığı dahil, istediği başlığı yazabilir. mila size dayanabileceğiniz tek bir kural verir:

mila'nın ilettiği her ham mesajda mila'nın yazdığı tam olarak bir Authentication-Results başlığı bulunur; authserv-id'si mx1.mila.cx'tir ve mesajdaki ilk Authentication-Results başlığı mila'nındır. Yalnızca bu ilk başlığa güvenin.

mila bunu her mesaj için uygular:

Yani: mesajdaki ilk Authentication-Results başlığını alın, authserv-id'sinin mx1.mila.cx olduğunu doğrulayın ve sonucu ondan okuyun; diğerlerini yok sayın.

MCP sunucusundan

mila'nın MCP sunucusunda on iki webhook aracı vardır: list_webhook_event_types, list_webhooks, get_webhook, create_webhook, update_webhook, delete_webhook, rotate_webhook_secret, send_webhook_test, list_webhook_deliveries, retry_webhook_delivery, retry_webhook_deliveries ve set_domain_inbound_webhook. Panel ve API ile aynı yönetim kuralı geçerlidir: yalnızca sahip ya da tüm alan adlarına erişimi olan bir yönetici, ve yalnızca tüm alan adları için verilmiş bir yetkilendirme ile.

Güvenlik