---
title: "iletiMerkezi Webhooks"
description: "Receive real-time SMS delivery reports on your server via POST. Setup, payload, status values."
slug: /en/docs/api/webhooks
locale: en
audience: developer
last_updated: 2026-04-29
auth: api-key-and-hash
related: [send-sms, get-report, authentication, error-codes]
alternates:
  tr: https://www.iletimerkezi.com/docs/api/webhooks
  en: https://www.iletimerkezi.com/en/docs/api/webhooks
  toplusmsapi: https://toplusmsapi.com/sms/rapor/liste/webhook
  a2psmsapi: https://a2psmsapi.com/en/sms/rapor/liste/webhook
---

# iletiMerkezi Webhooks

Webhooks deliver real-time status updates for the messages you sent via `send-sms`. You configure a URL in the panel; iletiMerkezi `POST`s to that URL on every status change (acceptance, delivery, failure). Webhooks are more efficient and lower-latency than polling `get-report`.

## Setup

1. Open `panel.iletimerkezi.com` → **Settings → API → Notification URL** (Turkish: "Bildirim Adresi").
2. Enter your webhook URL (e.g., `https://yourdomain.com/api/iletimerkezi/webhook`) and click **Add** to save.
3. The URL must be HTTPS and reachable from the public internet. iletiMerkezi servers will `POST` to it.

> Only **one webhook URL** per account is supported. If you need to dispatch the same report to multiple destinations, fan out from your webhook handler.

> Once the URL is set, every `send-sms` order's delivery reports flow into it automatically — no extra API call needed.

## Request

For each message status update, iletiMerkezi `POST`s to your URL with:

```http
POST https://yourdomain.com/api/iletimerkezi/webhook
Content-Type: application/json
```

```json
{
  "report": {
    "id": 1599558518,
    "packet_id": 104525848,
    "status": "accepted",
    "to": "+905XXXXXXXXX",
    "body": "Message text"
  }
}
```

### Fields

- `report.id` (integer): unique ID of a single message inside an order.
- `report.packet_id` (integer): the order ID this message belongs to. Matches `order.id` returned by `send-sms`; all webhook events from one order share this `packet_id`.
- `report.status` (string): message status (see table below).
- `report.to` (string): recipient number with `+90` prefix.
- `report.body` (string): the message text that was sent.

### Status values (`report.status`)

| Value | Meaning |
|---|---|
| `accepted` | Message was accepted and queued for delivery |
| `delivered` | Message was successfully delivered to the recipient |
| `undelivered` | Message could not be delivered (off network, blacklist, format, etc.) |

> `get-report` returns numeric codes (`110`, `111`, `112`); webhooks send string values (`accepted`, `delivered`, `undelivered`). For one message you typically receive `accepted` first, then either `delivered` or `undelivered`.

## Response

Your endpoint should reply with `2xx` (e.g., `200 OK`). A non-`2xx` response can cause iletiMerkezi to flag the webhook as failed; respond with `200` quickly and process work asynchronously.

```http
HTTP/1.1 200 OK
Content-Type: application/json

{"received": true}
```

## Security

iletiMerkezi webhooks currently **do not send an HMAC signature or `X-Signature`-style header**; the payload is the raw `report` object. You must enforce at least one verification layer on the receiver side in production.

**Recommended pattern: secret token in the URL.** Configure the webhook URL in the panel with a `token` query parameter; your handler compares that token against a stored value and rejects mismatches with `401`. This is the same pattern our internal services use to consume iletiMerkezi DLRs.

```http
POST https://yourdomain.com/api/iletimerkezi/webhook?token=SECRET_VALUE
```

```js
// Handler example (Node/Express)
app.post('/api/iletimerkezi/webhook', (req, res) => {
  if (req.query.token !== process.env.WEBHOOK_TOKEN) {
    return res.status(401).end();
  }
  // ... process payload
  res.status(200).json({ received: true });
});
```

**Extra defenses:**
- Treat the webhook URL like a secret — keep it in env vars, do not commit it.
- HTTPS is enforced by the panel; HTTP URLs are rejected.
- Match incoming `report.packet_id` against an order ID you wrote to your own DB. Silently drop reports you can't match (see "Multi-app filtering" below).

> Native HMAC signing is a roadmap candidate; it does not exist today. The URL-token convention is the real protection layer right now.

## Common pitfalls

- **All SMS on the account land on a single webhook URL (multi-app filtering).** If the same iletiMerkezi account is shared by multiple apps, you will receive DLRs that do not belong to your app. On the receiver side, look for an app-specific marker inside `report.body` (e.g., your short-link domain `iim.to/...`, a campaign tag, an order-ID prefix) and silently `200 OK` anything that doesn't match. Otherwise you will write orphan DLRs into your DB and treat them as your own.

  ```js
  // Practical example: short-link domain as the marker
  if (!report.body.includes('iim.to')) {
    return new Response('OK', { status: 200 });  // not ours, drop silently
  }
  ```

- **The same `report.id` can arrive more than once; write idempotently.** Carrier or iletiMerkezi-side retries can repeat a status update. Practical pattern: on the first callback, write `delivery_received_at` + status; on subsequent callbacks, only refresh the status (don't touch the timestamp). Idempotency key: `report.id + report.status`.
- **`packet_id` equals the `send-sms` `order.id`.** Use it to correlate webhooks with the originating order; multiple message webhooks from the same order share this `packet_id`.
- **`accepted` is not "delivered".** It means "accepted, queued". Wait for `delivered` for actual delivery; an `undelivered` webhook is the failure terminus.
- **Respond fast.** Push DB writes and downstream API calls onto a background queue, then return `200` from the webhook handler immediately. A slow handler triggers retries.
- **HTTPS only.** HTTP URLs are not accepted by the panel. Use ngrok or a cloudflared tunnel for local development.
- **No webhook = polling.** If the webhook URL is empty, you can only learn the delivery state via `get-report`. Webhooks are recommended in production; you can keep polling as a fallback.

## Related

- [Send SMS (send-sms)](./send-sms.md)
- [Order report (get-report)](./get-report.md)
- [Authentication](./authentication.md)
- [Error code table](./error-codes.md)
