Gelen Posta Webhook'ları
Webhook bildirimleri aracılığıyla ayrıştırılmış gelen e-posta verilerini alın
Gelen bir e-posta, actions alanında webhook bulunan bir rotayla eşleştiğinde, MailLand
ayrıştırılmış e-postayı o rotanın webhook'una bir JSON POST isteği olarak gönderir. Bu sayfa yük
biçimini, imza doğrulamasını ve teslimat davranışını kapsar.
Webhook Yükü
MailLand, webhook URL'nize aşağıdaki JSON gövdesiyle bir POST isteği gönderir:
{
"event": "inbound.received",
"event_id": "evt_a1b2c3d4e5f60718293a4b5c6d7e8f90",
"timestamp": "2026-02-17T14:30:00Z",
"occurred_at": "2026-02-17T14:29:58Z",
"schema_version": 1,
"data": {
"idempotency_key": "k7m2x9q4p1w8z3_r7k3m9q2p4w8x1",
"email": {
"id": "k7m2x9q4p1w8z3",
"received_at": "2026-02-17T14:29:58Z",
"size_bytes": 48213,
"envelope": {
"from": "[email protected]",
"to": ["[email protected]"]
},
"headers": {
"from": { "address": "[email protected]", "name": "Jane Customer" },
"to": [{ "address": "[email protected]", "name": null }],
"cc": [],
"subject": "Need help with my account",
"message_id": "<[email protected]>",
"in_reply_to": null,
"reply_to": "[email protected]",
"date": "2026-02-17T14:29:55Z"
},
"body": {
"text_preview": "I need assistance with my account settings.",
"has_text": true,
"has_html": true
},
"attachments": [
{
"id": "a3k7m2q9p4w1x8",
"file_name": "screenshot.png",
"content_type": "image/png",
"size_bytes": 34500,
"is_inline": false,
"content_id": null,
"download_url": "https://r2.example.com/screenshot.png?X-Amz-Signature=..."
}
],
"authentication": {
"spf": { "result": "pass" },
"dkim": { "result": "pass" },
"dmarc": { "result": "pass" }
},
"spam": {
"score": 1.2,
"is_spam": false,
"reason": null
},
"source": {
"ip": "203.0.113.42",
"hostname": "mail.example.com"
}
},
"matched_route": {
"id": "r7k3m9q2p4w8x1",
"name": "Support Inbox"
}
}
}E-postanın tam gövdesi hiçbir zaman satır içinde yer almaz: yalnızca kısaltılmış bir
text_preview ile has_text/has_html bayrakları bulunur. body.html alanı yoktur.
Değeri olmayan alanlar (name, in_reply_to, content_id, reason, data.matched_route vb.) null
olarak gönderilmek yerine JSON'dan tamamen çıkarılır. Yukarıdaki örnek okunabilirlik için
null gösterir; anahtarın var olacağına güvenmeyin.
Yük Alanları
| Alan | Tür | Açıklama |
|---|---|---|
event | string | Her zaman inbound.received |
event_id | string | Olay kimliği; X-MLND-Event-Id başlığına eşittir. Gelen ileti ve webhook başına sabittir (evt_ ve ardından 32 onaltılık karakter): aynı teslimatın her yeniden denemesi ve yeniden yayımı aynı event_id, başlık ve gövdeyi taşır. Elle yeniden oynatma yeni bir event_id taşır ve data.replay_of alanını doldurur. |
data.idempotency_key | string | Bir rota eşleştiğinde {emailPublicId}_{routePublicId}, eşleşmediğinde (veya rota artık mevcut değilse) yalnızca {emailPublicId}. Yeniden denemeler ve manuel tekrarlar boyunca sabittir |
data.replay_of | string | Yalnızca elle yeniden oynatmada bulunur: özgün teslimatın taşıdığı event_id. Normal teslimatlarda ve olay kimlikleri sabit hale gelmeden önce alınmış iletilerin yeniden oynatılmasında gönderilmez |
timestamp | string | Olayın oluşturulduğu ISO 8601 zaman damgası |
occurred_at | string | E-postanın alındığı an; data.email.received_at ile aynı an |
schema_version | number | Yük şema sürümü, 1; bkz. Sürümleme |
data.email.id | string | Gelen e-postanın genel kimliği |
data.email.received_at | string | E-postanın alındığı zaman |
data.email.size_bytes | number | Ham mesaj boyutu (bayt) |
data.email.envelope.from | string | SMTP MAIL FROM adresi |
data.email.envelope.to | array | SMTP RCPT TO adresleri |
data.email.headers.from | object | From başlığından ayrıştırılan { address, name } |
data.email.headers.to / .cc | array | { address, name } dizileri |
data.email.headers.subject | string | E-posta konusu |
data.email.headers.message_id | string | null | Message-ID başlığı |
data.email.headers.in_reply_to | string | null | In-Reply-To başlığı |
data.email.headers.reply_to | string | null | Reply-To başlığı |
data.email.headers.date | string | null | Date başlığı |
data.email.body.text_preview | string | null | Kısaltılmış düz metin önizlemesi |
data.email.body.has_text / .has_html | boolean | Metin veya HTML bölümünün olup olmadığı |
data.email.attachments | array | Aşağıya bakın |
data.email.authentication.spf/dkim/dmarc.result | string | Küçük harfli sonuç, ör. pass, fail, none |
data.email.spam.score | number | Spam puanı |
data.email.spam.is_spam | boolean | E-postanın spam olarak sınıflandırılıp sınıflandırılmadığı |
data.email.spam.reason | string | null | Mevcutsa sınıflandırma nedeni |
data.email.source.ip / .hostname | string | null | Gönderen sunucunun IP'si ve ters DNS ana bilgisayar adı |
data.matched_route | object | null | Eşleşen rotanın { id, name } bilgisi veya null |
Ekler
Ek içeriği hiçbir zaman satır içinde yer almaz. data.email.attachments içindeki her giriş bir
download_url taşır:
| Alan | Tür | Açıklama |
|---|---|---|
id | string | Ek kimliği. Kararlıdır: olayın her yeniden denemesi ve her yeniden oynatması aynı ek için aynı id değerini taşır; daha önce indirdiğiniz bir eki tanımak için bunu kullanın. download_url her denemede yeniden üretilir |
file_name | string | Orijinal dosya adı |
content_type | string | MIME türü |
size_bytes | number | Bayt cinsinden boyut |
is_inline | boolean | Ekin satır içinde referans verilip verilmediği (ör. gömülü bir görsel) |
content_id | string | null | Satır içi ekler için Content-ID |
download_url | string | null | Ön imzalı URL; yükün oluşturulmasından itibaren 15 dakika geçerlidir |
download_url, Authorization başlığı gerektirmez: URL'nin kendisi kimlik bilgisidir ve webhook
yükü oluşturulduktan 15 dakika sonra sona erer. Webhook'u aldığınızda ekleri hemen indirin. URL'nin
süresi dolmuşsa, bunun yerine ham mesajın tamamını alın:
GET https://api.mail.land/v1/transactional/inbound/emails/{emailId}/rawBu, normal Authorization başlığınızı gerektirir ve orijinal .eml dosyasını döndürür
(Content-Type: message/rfc822): ek başına indirme uç noktası yoktur.
İmza Doğrulama
Gelen webhook'lar diğer her MailLand webhook'u ile tamamen aynı şekilde imzalanır: aynı başlık, aynı şema, aynı sır işleme. Sekiz dilde işlenmiş bir örnek ve doğrulayıcı kod içeren kurallı referans Signature Verification sayfasıdır; orada yazılan her şey burada değişmeden geçerlidir.
Kısaca:
X-MLND-Signature: t=1708171200,v1=<hex>tsaniye cinsinden bir Unix zaman damgasıdır,v1küçük harfli onaltılık bir HMAC-SHA256'dır;- imzalanan dize
{timestamp}.{raw_body}şeklindedir: zaman damgası, gerçek bir nokta, ardından JSON ayrıştırmasından önce, alındığı haliyle gövde; - anahtar, webhook oluşturulduğunda bir kez gösterilen imzalama sırrıdır;
- sabit zamanda karşılaştırın ve 300 saniyeden eski bir zaman damgasını reddedin.
İmzasız bir teslimat yolu yoktur: bir webhook'un kullanılabilir bir sırrı yoksa, MailLand onu
imzasız göndermek yerine teslimatı başarısız kılar. X-MLND-Signature başlığı olmadan gelen her
isteği reddedin.
Teslimat Davranışı
Yanıt Kodları
Uç noktanız, alındığını onaylamak için bir 2xx durum koduyla yanıt vermelidir. Bunun dışındaki her
şey, 2xx olmayan bir durum veya zaman aşımı, başarısız bir teslimat girişimi olarak sayılır.
| Yanıtınız | MailLand Eylemi |
|---|---|
200–299 | Teslimat başarılı, yeniden deneme yok |
| Başka herhangi bir durum, veya 15 saniye içinde yanıt yoksa | Teslimat başarısız, yeniden denenir (aşağıdaki deneme sınırına tabi) |
Yeniden Deneme İlkesi
Başarısız bir deneme, diğer tüm webhook olaylarıyla aynı takvimle yeniden denenir, olay başına toplam 6 denemeye kadar: ilk deneme, ardından hemen, 1 dakika, 5 dakika, 15 dakika ve 1 saat sonra yeniden denemeler. 6. başarısız denemeden sonra, teslimat kalıcı olarak başarısız olarak işaretlenir. Bunu manuel olarak yeniden oynatabilirsiniz. Tam takvim için Yeniden Deneme Mantığı sayfasına bakın.
Yaklaşık bir buçuk saatten kısa süre kapalı kalan bir uç nokta bu pencere boyunca yeniden denenir, daha uzun bir kesinti ise kalıcı olarak başarısız bir teslimatla sonuçlanır. Uç noktanızı hatada bile hızlı yanıt verecek şekilde tasarlayın ve daha uzun kesintiler için manuel tekrara güvenin.
Her başarısız teslimat denemesi (her olay değil) webhook'un ardışık başarısızlık toplamına
sayılır ve bu toplam webhook'un kendi pauseThreshold değerine ulaştığında (0'dan büyük bir değere
ayarlanmışsa) ya da aksi halde varsayılan olan 10 ardışık başarısızlıkta webhook otomatik olarak
devre dışı bırakılır: bundan sonra yeni gelen e-postalar onun için hiçbir webhook teslimatı
kuyruğa almaz; bu aynı webhook satırı mesaj olayları için de kullanıldığından, onlar da durur.
Varsayılan eşikte, tamamen başarısız olan iki gelen e-posta (her biri 6 deneme) webhook'u devre
dışı bırakmaya yeter. Webhook'u Webhooks sayfasından yeniden etkinleştirin, ardından kaçırılan
e-postaları yeniden oynatın.
Bir Webhook'u Yeniden Oynatma
Belirli bir gelen e-posta için webhook'u yeniden teslim edin:
POST https://api.mail.land/v1/transactional/inbound/emails/{id}/replay-webhookcurl -X POST https://api.mail.land/v1/transactional/inbound/emails/k7m2x9q4p1w8z3/replay-webhook \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"emailId": "k7m2x9q4p1w8z3",
"queued": true,
"webhookUrl": "https://yourapp.com/webhooks/support",
"message": "Webhook replay queued successfully"
}Bu işlem, eşleşen rotanın webhook'una kalıcı bir teslimatı kuyruğa alır ve ilgili satır commit edildiğinde 202 Accepted döndürür; eşzamanlı gönderim yapmaz. Yeniden oynatma yeni bir olay kimliği alır (yeni bir event_id ve X-MLND-Event-Id) ve özgün teslimatın kimliğini data.replay_of içinde taşır; böylece tekrar eleme mekanizmanız onu yeni bir olay olarak görür, yine de özgün olayla ilişkilendirebilirsiniz. data.idempotency_key özgün teslimattakiyle aynı kalır. İstek X-MLND-Source: inbound ve X-MLND-Inbound-Email-Id başlıklarını taşır; yükteki her ek download_url gönderim anında imzalanır ve o andan itibaren 15 dakika geçerlidir, yeniden oynatmayı çağırdığınız andan itibaren değil.
Olayları tekrarlardan arındırma
Gelen webhook teslimatı en az bir kez (at-least-once) yapılır: yeniden deneme, isteği zaten işledikten sonra sizin tarafınızda oluşan zaman aşımı veya worker devri aynı olayı birden fazla kez teslim edebilir. Alıcılar tekrarları elemelidir.
- Her gelen webhook sabit bir olay kimliği taşır; bu kimlik
event_idalanında veX-MLND-Event-Idbaşlığındadır. Gelen ileti ve webhook başına belirleyicidir; yani aynı iletinin aynı webhook'a yapılan tüm teslimat denemeleri aynı kimliği taşır. - Bu kimliğe göre tekrarları eleyin. İşlediğiniz kimlikleri saklayın ve daha önce gördüğünüz kimliği atlayın.
- Elle yeniden oynatma yeni bir olaydır. Yeni bir
event_idve özgün kimliği tutan birdata.replay_ofalanı vardır. Özgün olay zaten işlendiyse yeniden oynatmanın yok sayılmasını istiyorsanızdata.replay_ofdeğerini de sakladığınız kimliklerle karşılaştırın.
app.post("/webhooks/support", express.raw({ type: "application/json" }), async (req, res) => {
// 1. Verify X-MLND-Signature first (see Signature Verification).
const eventId = req.header("X-MLND-Event-Id");
const client = await db.connect();
try {
await client.query("BEGIN");
// 2. Record the event id; the unique constraint makes this atomic.
const inserted = await client.query(
"INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT (event_id) DO NOTHING",
[eventId],
);
// 3. Only a first-time id enqueues work, in the SAME transaction (jobs table).
if (inserted.rowCount > 0) {
await client.query("INSERT INTO jobs (kind, payload) VALUES ($1, $2)", [
"process-inbound-email",
req.body.toString(),
]);
}
await client.query("COMMIT");
res.sendStatus(200); // new or duplicate: acknowledge
} catch (err) {
await client.query("ROLLBACK"); // id and job are both undone, so the retry is not a "duplicate"
res.sendStatus(500);
} finally {
client.release();
}
});Kimliği kaydetmek ve işi kuyruğa almak tek bir işlemde yapılmalıdır: kimlik önce kaydedilir ve kuyruğa alma başarısız olursa, MailLand'ın yeniden denemesi tekrar sayılıp atlanır ve e-posta kaybolur.
İşlenen kimlikleri en az en uzun yeniden deneme pencerenizin süresi kadar, ayrıca tetikleyebileceğiniz her yeniden oynatmayı da kapsayacak şekilde saklayın.
Webhook Başlıkları
Her webhook isteği şu başlıkları içerir:
| Başlık | Örnek | Açıklama |
|---|---|---|
Content-Type | application/json | |
X-MLND-Signature | t=1708171200,v1=abc123... | Zaman damgası ve HMAC-SHA256 imzası |
X-MLND-Event | inbound.received | Olay türü |
X-MLND-Event-Id | evt_a1b2c3d4... | Benzersiz olay kimliği |
X-MLND-Event-Version | 1 | Yük şema sürümü |
X-MLND-Webhook-Id | w4k8m2q7p9x3z1 | Webhook'un genel kimliği |
X-MLND-Attempt | 1 | Teslimat deneme numarası (1'den başlar) |
X-MLND-Source | inbound | Olayı hangi hattın ürettiği |
X-MLND-Inbound-Email-Id | k7m2x9q4p1w8z3 | Bu teslimatın ilgili olduğu gelen e-posta |
En İyi Uygulamalar
- Hızlı yanıt verin: webhook'u hemen
200ile onaylayın ve e-postayı arka planda bir işte asenkron olarak işleyin. - İmzaları doğrulayın: sahte yükleri önlemek için her zaman
X-MLND-Signaturebaşlığını doğrulayın. - Tekrarları yönetin: teslimat en az bir kez yapılır; bu yüzden
X-MLND-Event-Id(yüktekievent_id) ile tekrarları eleyin. Elle yeniden oynatmanın kimliği yenidir vedata.replay_ofözgün kimliği gösterir; bkz. Olayları tekrarlardan arındırma. - Ekleri hemen indirin: her
download_url, yük oluşturulduktan 15 dakika sonra sona erer; bundan sonra ham mesajı alın. - HTTPS kullanın: webhook URL'leri, güvenli yük teslimatı için üretimde HTTPS kullanmalıdır.
Hata Yanıtları
| Durum | Kod | Açıklama |
|---|---|---|
400 | INVALID_REQUEST | E-posta için eşleşen bir rota yok |
400 | INVALID_REQUEST | Rotanın yapılandırılmış bir webhook'u yok |
400 | INVALID_REQUEST | Rotanın webhook'u devre dışı: yeniden oynatmadan önce etkinleştirin |
404 | RESOURCE_NOT_FOUND | Yeniden oynatma için e-posta bulunamadı |
Sonraki Adımlar
- Inbound Overview: gelen posta ayarlarını yapılandırma ve e-postaları görüntüleme
- Inbound Routes: webhook teslimatı için yönlendirme kurallarını ayarlama
- Webhooks: e-posta olayları için giden webhook yapılandırması