> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lekalao.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscribers

> Subscribe, keep up to date, tag and unsubscribe from your own application.

The usual case: your shop subscribes its customers to the "Customers" list, tags them by what they buy, and passes unsubscribes back.

<Note>
  Every example uses `$LEKALAO_TOKEN` (a **Read and write** token) and
  `$LIST`, the `id` of the list. A list's `id` is in `GET /api/v1/lists`.
</Note>

## Subscribe one person

```bash theme={null}
curl -X POST https://lekalao.example.com/api/v1/lists/$LIST/subscribers \
  -H "Authorization: Bearer $LEKALAO_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "email": "ada@example.com",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "locale": "en",
    "timezone": "Africa/Douala",
    "attributes": { "city": "Douala", "customer_since": "2024-03-01" },
    "tags": ["customer", "wholemeal-bread"]
  }'
```

Answer `201`:

```json theme={null}
{
    "data": {
        "id": "5f0e7c1b-2d4a-4e8f-b1c3-9a7d6e5f4b3a",
        "email": "ada@example.com",
        "first_name": "Ada",
        "last_name": "Lovelace",
        "timezone": "Africa/Douala",
        "locale": "en",
        "status": "unconfirmed",
        "tags": ["customer", "wholemeal-bread"],
        "attributes": { "city": "Douala", "customer_since": "2024-03-01" },
        "subscribed_at": null,
        "unsubscribed_at": null,
        "created_at": "2026-09-17T10:02:11+00:00"
    }
}
```

**Keep the `id`** in your own database: it is what names the person in later calls.

### What happens, case by case

| Case                                      | Result                                                                        |
| ----------------------------------------- | ----------------------------------------------------------------------------- |
| New address, list without double opt-in   | `status: subscribed`. Subscription automations start.                         |
| New address, list with double opt-in      | `status: unconfirmed`, confirmation e-mail sent.                              |
| The same with `"skip_confirmation": true` | `status: subscribed` straight away.                                           |
| Address already subscribed                | Updated. Empty fields replace nothing, attributes are merged, tags are added. |
| Address that had unsubscribed             | **Subscribed again**, with confirmation if the list asks for it.              |
| Address on the suppression list           | `422` "This address is blocked."                                              |
| The plan's subscriber limit is reached    | `422` with the plan's message.                                                |

<Warning>
  `skip_confirmation` and bringing back people who unsubscribed are your
  responsibility. Subscribe directly only people whose consent you hold, and
  do not send someone who unsubscribed back to Lekalao: listen to the
  `subscriber.unsubscribed` webhook to keep your own database honest.
</Warning>

Addresses are stored in lower case, without surrounding spaces.

## Subscribe in a batch

Up to 1,000 people in one call, for a nightly synchronisation for example:

```bash theme={null}
curl -X POST https://lekalao.example.com/api/v1/lists/$LIST/subscribers/batch \
  -H "Authorization: Bearer $LEKALAO_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "subscribers": [
      { "email": "ada@example.com", "first_name": "Ada", "tags": ["customer"] },
      { "email": "grace@example.com", "first_name": "Grace", "attributes": { "city": "Yaoundé" } }
    ],
    "skip_confirmation": false,
    "replace_tags": false
  }'
```

**Up to 100 lines**, the answer comes back at once, line by line:

```json theme={null}
{
    "data": [
        {
            "index": 0,
            "email": "ada@example.com",
            "id": "5f0e…",
            "outcome": "already_subscribed"
        },
        {
            "index": 1,
            "email": "grace@example.com",
            "id": "8b2d…",
            "outcome": "pending"
        }
    ],
    "meta": { "added": 1, "updated": 1, "failed": 0 }
}
```

| `outcome`            | Means                                                             |
| -------------------- | ----------------------------------------------------------------- |
| `subscribed`         | Subscribed.                                                       |
| `pending`            | Waiting for confirmation.                                         |
| `already_subscribed` | Already there; details updated.                                   |
| `failed`             | Refused (blocked address, plan full). The reason is in `message`. |

One refused line does not spoil the others. If **all** of them fail, the code is `422`.

**Past 100 lines**, the batch goes to the background like an import. The answer is `202` with the import to follow:

```json theme={null}
{
    "data": {
        "id": "c41a…",
        "status": "pending",
        "file_name": "API batch of 850",
        "url": "https://lekalao.example.com/api/v1/imports/c41a…"
    },
    "meta": { "queued": 850 }
}
```

Poll `GET /api/v1/imports/{id}` until `status` is `completed` or `failed`. The `added`, `updated` and `failed` counters and the detail of the `errors` are there.

`replace_tags: true` replaces each person's tags with those of their line instead of adding them.

## Import a file

For a CSV or Excel file that is already prepared, send it as it is:

```bash theme={null}
curl -X POST https://lekalao.example.com/api/v1/lists/$LIST/imports \
  -H "Authorization: Bearer $LEKALAO_TOKEN" \
  -H "Accept: application/json" \
  -F "file=@customers.csv" \
  -F "tags[]=september-import" \
  -F "mapping[E-mail]=email" \
  -F "mapping[First name]=first_name" \
  -F "mapping[City]=attribute:city"
```

Without `mapping`, columns are recognised from their headers. Imported people are subscribed **without a confirmation e-mail**, and those who had unsubscribed come back only when `resubscribe_unsubscribed` is `true`. See [Import and export](/contacts/import-export).

## Find someone

```bash theme={null}
# By address
curl "https://lekalao.example.com/api/v1/lists/$LIST/subscribers?email=ada@example.com" \
  -H "Authorization: Bearer $LEKALAO_TOKEN" -H "Accept: application/json"

# Yesterday's new subscribers, by tag
curl "https://lekalao.example.com/api/v1/lists/$LIST/subscribers?status=subscribed&tag=customer&since=2026-09-16" \
  -H "Authorization: Bearer $LEKALAO_TOKEN" -H "Accept: application/json"
```

| Filter              | Values                                                    |
| ------------------- | --------------------------------------------------------- |
| `status`            | `subscribed`, `unconfirmed`, `unsubscribed`               |
| `email`             | One exact address.                                        |
| `tag`               | The name of a tag.                                        |
| `since`             | Created since that date.                                  |
| `sort`, `direction` | `email` or creation order; `asc` or `desc` (the default). |

`GET /api/v1/subscribers/{id}` reads one person.

## Update

```bash theme={null}
curl -X PUT https://lekalao.example.com/api/v1/subscribers/$SUBSCRIBER \
  -H "Authorization: Bearer $LEKALAO_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "first_name": "Ada", "locale": "fr", "attributes": { "city": "Kribi", "points": 120 } }'
```

* A field left out, or `null`, changes nothing.
* `attributes`, when sent, **replaces** every attribute.
* `tags`, when sent, **replaces** every tag.

## Add and remove tags

To add or remove without knowing the other tags:

```bash theme={null}
curl -X POST https://lekalao.example.com/api/v1/subscribers/$SUBSCRIBER/tags \
  -H "Authorization: Bearer $LEKALAO_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "add": ["abandoned-basket"], "remove": ["ordered"] }'
```

A tag the list does not have yet is created. Adding a tag can [start an automation](/automations/triggers).

The tags of a list are read with `GET /api/v1/lists/{id}/tags` and created in advance with `POST /api/v1/lists/{id}/tags`.

## Unsubscribe, subscribe again, delete

| Call                                         | Effect                                                                              |
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
| `POST /subscribers/{id}/unsubscribe`         | Unsubscribes. The person stays on the list and in the statistics.                   |
| `POST /subscribers/{id}/resubscribe`         | Subscribes again **without confirmation**. `422` if the address is blocked.         |
| `POST /subscribers/{id}/resend-confirmation` | Sends the confirmation e-mail again. `422` if the person is not waiting to confirm. |
| `DELETE /subscribers/{id}`                   | Deletes the record. Prefer unsubscribing, which keeps the history.                  |

## In PHP

```php theme={null}
use Illuminate\Support\Facades\Http;

$lekalao = Http::withToken(config('services.lekalao.token'))
    ->baseUrl('https://lekalao.example.com/api/v1')
    ->acceptJson()
    ->retry(3, 500, throw: false);

$response = $lekalao->post("lists/{$listUuid}/subscribers", [
    'email' => $customer->email,
    'first_name' => $customer->first_name,
    'locale' => $customer->locale,
    'tags' => ['customer'],
]);

if ($response->successful()) {
    $customer->update(['lekalao_id' => $response->json('data.id')]);
}
```

## In JavaScript (Node)

```js theme={null}
const response = await fetch(
    `https://lekalao.example.com/api/v1/lists/${listUuid}/subscribers`,
    {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${process.env.LEKALAO_TOKEN}`,
            'Content-Type': 'application/json',
            Accept: 'application/json',
        },
        body: JSON.stringify({
            email: 'ada@example.com',
            first_name: 'Ada',
            tags: ['customer'],
        }),
    },
);

if (response.status === 422) {
    const { message, errors } = await response.json();
    console.warn(message, errors);
}
```
