# Email Verification API

Verify email addresses from anywhere over HTTP. Same verification engine the
dashboard uses (`App\Library\verifyEmail`), same credit accounting.

Base URL: `https://your-domain.com/api/v1`

## Setup

1. Upload the files. Nothing needs to be recompiled or cleared:
   `bootstrap/cache/` holds no config or route cache, Blade recompiles views on
   change, and `composer.json` does not set `classmap-authoritative`, so
   Composer's PSR-4 rule resolves the new `App\Services` and
   `App\Http\Controllers\Api\V1` classes without a `dump-autoload`.

2. Make sure the `personal_access_tokens` table exists. On a fresh install the
   Laravel installer already created it. If it is missing, either run

   ```bash
   php artisan migrate
   ```

   or, with no shell access, import `database/personal_access_tokens.sql`
   through phpMyAdmin — it creates the table and records the migration as
   applied.

3. Create a key from **Dashboard → API Keys**, or from the CLI:

   ```bash
   php artisan api:token user@example.com --name="prod server"
   php artisan api:token user@example.com --list
   php artisan api:token user@example.com --revoke=7
   ```

The plain-text token is shown once. Only a hash is stored.

## Authentication

Every request needs:

```
Authorization: Bearer YOUR_KEY
Accept: application/json
```

Tokens do not expire unless `expiration` is set in `config/sanctum.php`.

## Endpoints

### `GET /api/v1/ping`

Confirms a key works and reports the account it belongs to.

```json
{
  "success": true,
  "data": {
    "account": { "id": 1, "name": "Faz", "email": "user@example.com", "role": "user" },
    "token_name": "prod server",
    "credits_remaining": 5000,
    "bulk_limit": 25
  }
}
```

### `GET /api/v1/credits`

```json
{ "success": true, "data": { "credits_remaining": 5000, "unlimited": false } }
```

`credits_remaining` is `null` and `unlimited` is `true` for admin accounts.

### `POST /api/v1/verify`

| Field   | Type   | Required | Notes                |
| ------- | ------ | -------- | -------------------- |
| `email` | string | yes      | The address to check |

```bash
curl -X POST https://your-domain.com/api/v1/verify \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"email":"someone@example.com"}'
```

```json
{
  "success": true,
  "data": {
    "email": "someone@example.com",
    "status": "valid",
    "result": "deliverable",
    "reason": "Valid",
    "domain": "example.com",
    "format_valid": true,
    "no_reply": false,
    "disposable": false,
    "role_based": false
  },
  "credits_remaining": 4999
}
```

### `POST /api/v1/verify/bulk`

| Field    | Type              | Required | Notes                                |
| -------- | ----------------- | -------- | ------------------------------------ |
| `emails` | array of strings  | yes      | 1 to `API_BULK_LIMIT` (default 25)   |

Duplicates are removed before verification. The whole request is answered
synchronously, so allow a generous client timeout — budget roughly 2 seconds
per address.

```bash
curl -X POST https://your-domain.com/api/v1/verify/bulk \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"emails":["a@example.com","support@example.com","bad@@format"]}'
```

```json
{
  "success": true,
  "data": [
    { "email": "a@example.com", "status": "valid", "result": "deliverable", "...": "..." },
    { "email": "support@example.com", "status": "valid", "result": "deliverable", "role_based": true, "...": "..." },
    { "email": "bad@@format", "status": "invalid_format", "result": "undeliverable", "...": "..." }
  ],
  "summary": { "total": 3, "deliverable": 2, "undeliverable": 1, "risky": 0, "unknown": 0 },
  "credits_charged": 2,
  "credits_remaining": 4997
}
```

For lists larger than the bulk limit, use the dashboard's file upload — it
queues the work through `App\Jobs\EmailCleaner` instead of blocking on HTTP.

## Result values

`result` is the coarse bucket to branch on; `status` is the specific outcome and
`reason` is the raw string from the verification engine.

| `result`        | `status`                                                            | Meaning                                        |
| --------------- | ------------------------------------------------------------------- | ---------------------------------------------- |
| `deliverable`   | `valid`                                                             | Mail server accepted the recipient             |
| `undeliverable` | `invalid`, `invalid_format`, `invalid_domain`, `bad_mx`, `no_reply`  | Rejected, no MX record, or a no-reply address   |
| `risky`         | `suspected`, `catch_all`                                            | Server would not give a definite answer        |
| `unknown`       | `timeout`                                                           | Temporary failure — safe to retry later        |

Flags returned alongside every result: `format_valid`, `no_reply`,
`disposable`, `role_based`. These come from the same helper lists the dashboard
uses (`noReplyArray`, `DisposableArray`, `roleBaseArray` in
`app/Helpers/helper.php`), so API and dashboard results agree.

## Credits

- One credit per address that reaches the SMTP check and gets a definite answer.
- Malformed addresses, no-reply addresses, and `unknown` results are **not**
  charged, so a retry after a timeout costs nothing extra.
- Bulk requests are rejected with `402` before doing any work if the account
  cannot cover the whole list; `credits_charged` reports the actual spend.
- Accounts with `role = admin` are not metered, matching the upload flow in
  `App\Http\Controllers\Admin\EmailController`.

## Errors

All errors share one shape:

```json
{ "success": false, "error": { "code": "insufficient_credits", "message": "..." } }
```

| HTTP  | `error.code`            | Cause                                     |
| ----- | ----------------------- | ----------------------------------------- |
| `401` | —                       | Missing, malformed, or revoked token      |
| `402` | `insufficient_credits`  | Not enough credits for the request        |
| `422` | `validation_failed`     | Bad input; see the `errors` object        |
| `429` | —                       | Rate limited; see `Retry-After` header    |

## Configuration

`config/api.php`, overridable in `.env`:

| Variable          | Default | Purpose                                        |
| ----------------- | ------- | ---------------------------------------------- |
| `API_BULK_LIMIT`  | `25`    | Max addresses per bulk request                 |
| `API_BULK_DELAY`  | `1`     | Seconds between SMTP probes in a bulk request  |
| `API_RATE_LIMIT`  | `30`    | Verification requests per minute, per token    |

The bulk limit is bounded by PHP's `max_execution_time` (120s in `.user.ini` on
this host). Raise that first if you raise `API_BULK_LIMIT`.

## Notes

- Many mail providers throttle or greylist SMTP probes from unfamiliar IPs.
  A `risky`/`unknown` result often means the provider declined to answer rather
  than that the address is bad.
- Set `Settings → General → Email Cleaner` to a real address on your sending
  domain; it is used as the `MAIL FROM` in the SMTP probe and providers reject
  probes from addresses that do not resolve.
