
# E-posta sağlayıcısını değiştir

`PUT /v1/email-provider` · izin `email-provider:write` · tüm planlar

Ortamın e-postayı nasıl gönderdiğini değiştirir. Beş mod belirtebilirsiniz:

- `platform_default`: KeenAuth, kendi adresinden gönderir. Yalnızca `reply_to` ayarlanabilir.
- `platform_managed_domain`: KeenAuth, kendi alan adınızdan gönderir ve bu alan adı için DKIM ile imzalar. Önce alan adını [e-posta gönderen alan adları](../email-sender-domains/create.md) üzerinden ekleyin ve DNS kayıtlarını yayımlayın; alan adının durumu `verified` olmalıdır. `sender_domain_id` ve tam olarak bu alan adına ait bir `from_address` ayarlayın (alt alan adı olmamalıdır). Yalnızca üretim ortamlarında kullanılabilir. Alan adı daha sonra doğrulanmış durumunu kaybederse, alan adı yeniden doğrulanana kadar e-postalar `platform_default` ile gönderilir; yapılandırma kendisi değişmez.
- `customer_smtp`: `465` (örtük TLS) veya `587` (STARTTLS zorunludur) numaralı bağlantı noktasındaki SMTP sunucunuz. Sertifika, sunucu adı için geçerli olmalıdır.
- `customer_api`: `ses`, `sendgrid`, `postmark`, `mailgun` veya `brevo` üzerindeki hesabınız.
- `customer_webhook`: KeenAuth iletiyi oluşturur ve [webhook uç noktalarınızdan](../webhook-endpoints/create.md) birine iletir; iletiyi uygulamanız gönderir (bkz. [Webhook teslimi](#webhook-teslimi)). `webhook_endpoint_id` alanına bu ortamdaki etkin bir uç noktanın kimliğini yazın.

`If-Match` zorunludur ve okuduğunuz sürümü taşır ([get](get.md) işleminden gelen `ETag`). Hiç yapılandırılmamış bir ortam için `If-Match: "0"` gönderin; `*` bu değerle eşleşmez. Eşleşme olmazsa `412` döner.

Kimlik bilgisi yalnızca yazılabilir. `credentials` alanında gönderin; hiçbir yanıt bunu geri döndürmez. Bu alanı atlarsanız, kayıtlı kimlik bilgisi yalnızca mod ve hedef (SMTP sunucusu, bağlantı noktası ve kullanıcı adı ya da sağlayıcı) değişmediğinde korunur. Aksi halde istek `/credentials` `required` hatasıyla başarısız olur.

Bir değişiklik, son bağlantı testi sonucunu temizler ve yalnızca mod, sağlayıcı ve gönderen alan adı kimliğini içeren bir denetim kaydı (`email_provider.updated`) yazar; adres ya da kimlik bilgisi asla yazılmaz. Değişiklikten sonra bir [bağlantı testi](test.md) çalıştırın.

## İstek

| Alan | Tür | Zorunlu | Açıklama |
|---|---|---|---|
| `mode` | string | evet | `platform_default`, `platform_managed_domain`, `customer_smtp`, `customer_api` veya `customer_webhook`. |
| `sender_domain_id` | string | `platform_managed_domain` | Bu ortamın `verified` durumundaki bir gönderen alan adı (`sdom_…`). Diğer modlarda kullanılamaz. |
| `webhook_endpoint_id` | string | `customer_webhook` | Bu ortamın etkin bir [webhook uç noktası](../webhook-endpoints/get.md) (`whe_…`). Diğer modlarda kullanılamaz. |
| `from_address` | string | `platform_default` dışındaki tüm modlar | `noreply@example.com` gibi çıplak gönderen adresi. `platform_managed_domain` modunda alan adı tam olarak gönderen alan adı olmalıdır. `platform_default` modunda kullanılamaz. |
| `reply_to` | string | hayır | Çıplak yanıt adresi. |
| `provider` | string | `customer_api` | `ses`, `sendgrid`, `postmark`, `mailgun` veya `brevo`. |
| `smtp_host` | string | `customer_smtp` | En az iki etiketli DNS adı (IP adresi değil). |
| `smtp_port` | integer | `customer_smtp` | `465` veya `587`. |
| `smtp_username` | string | `customer_smtp` | SMTP AUTH kullanıcı adı, 1 ila 256 karakter. |
| `credentials` | string | yukarıya bakın | Yalnızca yazılabilir, 1 ila 1024 bayt. `customer_smtp`: SMTP parolası. `customer_api`: `ses` için `<region>:<access key id>:<secret access key>`; `sendgrid` ve `mailgun` için API anahtarı (sağlayıcının AB uç noktası için önek `eu:`); `postmark` için sunucu belirteci; `brevo` için API anahtarı. |

## Yanıt

Kayıtlı yapılandırmayı içeren `200 OK` (şekli [get](get.md) ile aynı) ve yeni sürüme ayarlanmış `ETag`.

## Hatalar

| HTTP | kod | Ne zaman |
|---|---|---|
| 400 | `unknown_field` | İstek gövdesinde bilinmeyen bir alan var ya da JSON nesnesi değil. |
| 412 | `precondition_failed` | `If-Match`, kayıtlı sürüm değil (arada biri kaydetti) ya da ilk yazmadan önce `*`. |
| 413 | `payload_too_large` | İstek gövdesi 16 KiB'den büyük. |
| 428 | `precondition_required` | `If-Match` eksik. |
| 422 | `validation_failed` | Bir alan doğrulamayı geçemedi; `errors[]` alanı ve kuralı belirtir (aşağıya bakın). |
| 401 | `unauthenticated` | Kimlik bilgisi eksik, geçersiz ya da iptal edilmiş. |
| 403 | `insufficient_permissions` | Kimlik bilgisinin gerekli izni yok. |
| 429 | `rate_limited` | Hız sınırı aşıldı; `Retry-After` süresinden sonra yeniden deneyin. |
| 503 | `service_unavailable` | Bir bağımlılık geçici olarak kullanılamıyor; `Retry-After` süresinden sonra yeniden deneyin. |

`errors[]` içindeki kurallar:

| İşaretçi | Kural | Anlamı |
|---|---|---|
| `/mode` | `required`, `invalid` | Mod eksik ya da bilinmiyor. |
| `/mode` | `delivery_mode_unavailable` | Mod bu dağıtımda gönderemez: `customer_webhook` için webhook teslim bağlantısının etkin olması, `platform_managed_domain` için DKIM imzalamanın etkin olması, müşteri modları için ise giden teslimin etkin olması gerekir. |
| `/mode` | `production_only` | `platform_managed_domain` yalnızca üretim ortamlarında kullanılabilir. |
| `/mode` | `fields_not_allowed` | Modun kabul etmediği bir alan gönderildi. `customer_webhook`, `provider`, SMTP alanlarını, `credentials` veya `sender_domain_id` alanını kabul etmez. |
| `/sender_domain_id` | `required`, `invalid` | `platform_managed_domain` gönderen alan adı kimliği olmadan gönderildi ya da kimlik bir `sdom_…` kimliği değil. |
| `/sender_domain_id` | `not_found` | Bu ortamda bu kimliğe sahip bir gönderen alan adı yok. Başka bir ortamın alan adı da aynı yanıtı alır. |
| `/sender_domain_id` | `not_verified` | Gönderen alan adı var ama `verified` değil (DNS kayıtları bekliyor, bozulmuş ya da devre dışı). |
| `/from_address` | `not_in_sender_domain` | Gönderen adresi tam olarak gönderen alan adına ait değil. |
| `/webhook_endpoint_id` | `required`, `invalid` | `customer_webhook` uç nokta kimliği olmadan gönderildi ya da kimlik bir `whe_…` kimliği değil. |
| `/webhook_endpoint_id` | `not_found` | Bu ortamda bu kimliğe sahip bir webhook uç noktası yok. Başka bir ortamın uç noktası da aynı yanıtı alır. |
| `/webhook_endpoint_id` | `not_enabled` | Uç nokta var ama etkin değil. |
| `/credentials` | `required` | Mod ya da hedef değişti ve yeni kimlik bilgisi gönderilmedi. |
| `/credentials` | `invalid` | Kimlik bilgisi boş, çok uzun, denetim karakterleri içeriyor ya da sağlayıcının biçimine uymuyor. |
| `/from_address`, `/reply_to`, `/provider`, `/smtp_host`, `/smtp_port`, `/smtp_username` | `required`, `invalid` | Alan eksik ya da modda geçersiz. |

## Webhook teslimi

`customer_webhook` modunda KeenAuth iletiyi göndermez ve hiçbir zaman kendi gönderimine geri dönmez. Oluşturduğu her ileti (konu, HTML ve metin) için uç noktaya HTTPS üzerinden imzalı bir olay gönderir:

- İstek `POST` yöntemiyle, `Content-Type: application/json` ve Standard Webhooks v1 üstbilgileri `webhook-id`, `webhook-timestamp` ve `webhook-signature` ile gönderilir. `webhook-id`, ileti kimliğidir (`msg_…`) ve aynı iletinin her denemesinde aynıdır; bu nedenle yinelemeleri bu kimliğe göre ayıklayın. Olay türünün uç noktanın olay aboneliğinde bulunması gerekmez: uç nokta, teslim uç noktası olduğu için olayı alır.
- Başarı, herhangi bir `2xx` yanıttır. Yönlendirmeler izlenmez ve başarısızlık sayılır. Tek bir deneme 10 saniyede zaman aşımına uğrar.
- Başarısız bir deneme 15 saniye sonra yeniden denenir; toplamda en fazla 3 deneme yapılır. Üçüncü başarısızlıktan sonra ileti başarısız olarak işaretlenir ve e-posta gönderilmez. Yapılandırmayı kaydettikten sonra silinen ya da devre dışı bırakılan bir uç nokta, iletiyi deneme yapmadan hemen başarısız kılar.
- İleti içeriği webhook hizmeti tarafından saklanmaz. Yalnızca güvendiğiniz uç noktalara gönderin: gövde, alıcı adresini ve varsa oturum açma kodunu ya da bağlantısını içerir.

Gövde (`schema_version` 1):

```json
{
  "id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "type": "email.send_requested",
  "schema_version": 1,
  "environment_id": "env_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "occurred_at": "2026-10-27T09:00:00Z",
  "data": {
    "message_id": "msg_01k7xq3m5r8t9v0w1x2y3z4a5b",
    "family": "email_otp",
    "locale": "en",
    "to": "ada@example.com",
    "from": "auth@example.com",
    "from_name": "Example",
    "reply_to": "support@example.com",
    "subject": "Your sign-in code",
    "html": "<p>Your code is 123456</p>",
    "text": "Your code is 123456",
    "link": "https://example.keenauth.app/...",
    "code": "123456"
  }
}
```

| `data` alanı | Açıklama |
|---|---|
| `message_id`, `family`, `locale` | İleti kimliği (zarftaki `id` ile aynı), şablon ailesi ve iletinin oluşturulduğu dil. |
| `to` | Alıcı adresi. |
| `from`, `from_name`, `reply_to` | Yalnızca ayarlandığında bulunur: `from`, bu yapılandırmanın `from_address` değeridir. |
| `subject` | Oluşturulmuş konu. |
| `html`, `text` | Oluşturulmuş içerik; en az biri bulunur. |
| `link`, `code` | Yalnızca iletide oturum açma bağlantısı ya da tek kullanımlık kod varsa bulunur. |

İmza, `v1,` ile birlikte `HMAC-SHA256(key, "<webhook-id>.<webhook-timestamp>.<raw body>")` değerinin base64 karşılığıdır. Anahtar, uç noktanın imzalama sırrındaki `idp_whsec_` öneki ardından gelen 43 base62 karakterinin kodladığı 32 baytlık değerdir. Ham istek gövdesini ayrıştırmadan önce doğrulayın ve saatinizden 300 saniyeden fazla uzak bir zaman damgasını reddedin. Sır döndürme sırasında `webhook-signature` boşlukla ayrılmış iki imza içerir.

Python:

```python
import base64, hashlib, hmac, time

ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
PREFIX = "idp_whsec_"


def signing_key(secret: str) -> bytes:
    """The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix."""
    body = secret[len(PREFIX):len(PREFIX) + 43]
    n = 0
    for ch in body:
        n = n * 62 + ALPHABET.index(ch)
    return n.to_bytes(32, "big")


def verify(secret: str, headers: dict, raw_body: bytes, tolerance: int = 300) -> bool:
    msg_id = headers["webhook-id"]
    ts = headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > tolerance:
        return False
    signed = f"{msg_id}.{ts}.".encode() + raw_body
    mac = hmac.new(signing_key(secret), signed, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(mac).decode()
    # webhook-signature holds one or more space-separated signatures.
    return any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split(" "))
```

Node.js:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const PREFIX = "idp_whsec_";

// The HMAC key is the 32-byte value behind the 43 base62 characters after the prefix.
function signingKey(secret) {
  let n = 0n;
  for (const ch of secret.slice(PREFIX.length, PREFIX.length + 43)) n = n * 62n + BigInt(ALPHABET.indexOf(ch));
  return Buffer.from(n.toString(16).padStart(64, "0"), "hex");
}

export function verify(secret, headers, rawBody, tolerance = 300) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
  const mac = createHmac("sha256", signingKey(secret)).update(`${id}.${ts}.`).update(rawBody).digest();
  const expected = Buffer.from(`v1,${mac.toString("base64")}`);
  // webhook-signature holds one or more space-separated signatures.
  return headers["webhook-signature"].split(" ").some((s) => {
    const got = Buffer.from(s);
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
```

## Örnek

```bash
curl -X PUT https://api.eu.keenauth.com/v1/email-provider \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "0"' \
  -d @email-provider.json
```

`email-provider.json`:

```json
{
  "mode": "customer_smtp",
  "from_address": "no-reply@example.com",
  "smtp_host": "smtp.example.com",
  "smtp_port": 587,
  "smtp_username": "mailer",
  "credentials": "<SMTP password>"
}
```

İletileri bir webhook uç noktasına iletmek için:

```json
{
  "mode": "customer_webhook",
  "webhook_endpoint_id": "whe_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@example.com"
}
```

Bu modda [test](test.md) ile bağlantı yoktur; uç noktayı [Webhook uç noktasını test et](../webhook-endpoints/test.md) ile test edin.

Doğrulanmış gönderen alan adınızdan göndermek için:

```json
{
  "mode": "platform_managed_domain",
  "sender_domain_id": "sdom_01k7xq3m5r8t9v0w1x2y3z4a5b",
  "from_address": "auth@mail.example.com",
  "reply_to": "support@example.com"
}
```

Bu modda [test](test.md) ile bağlantı yoktur; bunun yerine alan adını [doğrula](../email-sender-domains/verify.md) ile kontrol edin.

CLI: `keenauth email-provider put --if-match '"0"' --data @email-provider.json`. Kimlik bilgisini bir dosyada tutun ya da standart girdi üzerinden verin (`--data -`), böylece komut geçmişinize yazılmasın.
