## How bulk reports differ

By default one request per message arrives at your address. At high volumes that
means thousands of requests — the bulk variant groups them into batches.

| | One by one | In bulk |
|:--|:--|:--|
| Method | HTTP GET | HTTP POST |
| Format | Parameters in the query string | JSON in the request body |
| Content | A single message | An array of records |

Both carry the same thing — delivery reports as well as incoming messages. The
one-by-one variant is described on
[Delivery confirmations and incoming SMS](https://www.bulkgate.com/en/docs/api-administration-tokens/api-delivery-confirmations-and-incoming-sms/).

## How to turn bulk sending on

Tick `Bulk DLRs - bulk request` in the webhook settings. The address and the
other options stay the same — only the way the data reaches you changes.

![The Delivery reports setting with the bulk sending option ticked](docs:api-bulk-delivery-confirmations-and-incoming-sms/bulk-setting_en)

The whole procedure, including where to find the setting, is described on
[Webhooks](https://www.bulkgate.com/en/docs/api-administration-tokens/webhooks/).

## What the request looks like

It arrives by POST with the `Content-Type: application/json` header. The body is
an array mixing delivery reports and incoming messages — you tell them apart by
`status`.

```http-json
POST /hook/delivery-report HTTP/1.1
Host: example.com
Content-Type: application/json

[
   {
      "status": "1",
      "smsID": "3t2563af3ff285",
      "price": "0.71",
      "date": "1904111007;1904111011"
   },
   {
      "status": "2",
      "smsID": "2t2563af3ff285",
      "price": "0.71",
      "date": "1904111007;1904111011"
   },
   {
      "status": "10",
      "from": "420777777777",
      "message": "text of the reply"
   }
]
```

## Record parameters

| Parameter | Value | When it arrives |
|:--|:--|:--|
| `status` | The status code — see [delivery statuses](#delivery-statuses) | Always |
| `smsID` | The unique ID of the message | With a delivery report |
| `price` | The price of the message in credits | With a delivery report |
| `date` | The time of sending and of delivery separated by a semicolon, CET timezone | With a delivery report |
| `from` | The sender's phone number | Only with `status` `10` |
| `message` | The text of the message in UTF-8 | Only with `status` `10` |

The `date` value has the form `YYMMDDHHMM;YYMMDDHHMM` — so `1904111007;1904111011`
means sent on 11 April 2019 at 10:07 and delivered at 10:11.

## Delivery statuses

| Code | Meaning |
|:--|:--|
| `1` | The message was delivered |
| `2` | The message is queued on the operator's side — the recipient is unavailable at the moment and delivery will be retried |
| `3` | The message could not be delivered — the recipient is unknown or permanently unavailable |
| `10` | An incoming message or a reply, not a delivery report |
| `13` | The message was read (Viber only) |

> [!NOTE]
> Status `2` is not final — another record with the resulting status arrives for
> the same message later. Expect to see the same `smsID` more than once, possibly
> even within the same batch.

## An example of processing

The script reads the request body, walks the array and routes each record by
`status`:

```php
<?php declare(strict_types=1);

const STATUS = [
    1 => 'delivered',
    2 => 'queued at operator',
    3 => 'not delivered',
    13 => 'read',
];

function log_line(string $file, string $message): void
{
    file_put_contents(
        __DIR__ . "/$file.log",
        '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL,
        FILE_APPEND
    );
}

function parse_date(string $value): string
{
    // the form YYMMDDHHMM, for example 1904111007
    $date = DateTimeImmutable::createFromFormat('ymdHi', $value);

    return $date ? $date->format('Y-m-d H:i') : '?';
}

$body = file_get_contents('php://input');
$records = json_decode($body ?: '', true);

if (!is_array($records))
{
    http_response_code(400);
    exit;
}

foreach ($records as $record)
{
    $status = (int) ($record['status'] ?? 0);

    if ($status === 10) // an incoming message
    {
        log_line('inbox', sprintf(
            'From: [%s] Message: [%s]',
            $record['from'] ?? '',
            $record['message'] ?? ''
        ));
    }
    else if (isset(STATUS[$status])) // a delivery report
    {
        [$sent, $delivered] = array_pad(explode(';', $record['date'] ?? ''), 2, '');

        log_line('delivery', sprintf(
            'Message [%s]: %s, price %.4f, sent %s, delivered %s',
            $record['smsID'] ?? '',
            STATUS[$status],
            (float) ($record['price'] ?? 0),
            parse_date($sent),
            parse_date($delivered)
        ));
    }
}

http_response_code(200);
```

> [!WARNING]
> The address has to answer quickly and with the status code `200`. The whole
> batch waits on a single response, so push heavy processing onto a queue and
> acknowledge the request right away.
