# NetArz Messaging API — Integration Guide

«رابط پیام‌رسانی نِت اَرز» sends **login codes, verification codes and transactional
messages by SMS and email** for the NetArz group's own sites. You do not need an SMS
panel, a sender line, an SMTP account or a code table of your own: one key, one HTTP
call, and NetArz generates the code, delivers it, and later tells you whether the
code the user typed is right.

- API base URL: `https://netarz.ir/api/msg/v1`
- Interactive reference: https://netarz.ir/docs/msg
- This guide in Markdown: https://netarz.ir/docs/msg.md
- Recipients: Iranian mobile numbers (`09xxxxxxxxx`, `+989…`, `989…` and Persian
  digits are all accepted) and ordinary email addresses.

> Access is limited. Apps are created by the NetArz admin for the group's own sites;
> there is no public sign-up. If you have a key, your app already exists.

---

## 1. Authentication

```
Authorization: Bearer sk_msg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: application/json
```

The key is a server-side secret. Never ship it to a browser or a mobile app — your
backend calls NetArz, your frontend only talks to your backend. NetArz stores only a
hash of the key; a lost key is replaced, not recovered.

```bash
curl https://netarz.ir/api/msg/v1/account -H "Authorization: Bearer $NETARZ_MSG_SECRET"
```

`GET /account` returns your channels, sender, caps and today's usage.

---

## 2. Login / verification codes (the main use)

The flow is two calls, and **your database never holds the code**:

1. User enters a mobile or email → your server calls `POST /otp`.
2. NetArz sends the code and returns an `id`. Keep it (session, or a hidden form field).
3. User types the code → your server calls `POST /otp/verify` with the `id` and the code.
4. `200 {"verified": true}` → sign the user in. Anything else → show the error message.

### 2.1 Send a code — `POST /otp`

```bash
curl -X POST https://netarz.ir/api/msg/v1/otp \
  -H "Authorization: Bearer $NETARZ_MSG_SECRET" -H "Content-Type: application/json" \
  -d '{"channel": "sms", "to": "09121234567", "purpose": "login"}'
```

| field | required | meaning |
|---|---|---|
| `channel` | yes | `sms` or `email` |
| `to` | yes | mobile number for `sms`, email address for `email` |
| `purpose` | no | `login` (default), `register`, `verify`, `reset`, `confirm` — changes the wording: «کد ورود»، «کد ثبت‌نام»، «کد تأیید»، «کد بازیابی رمز» |
| `length` | no | digits in the code, 4–8, default 6 |
| `ttl` | no | minutes the code stays valid, 1–30; default 2 for SMS, 10 for email |
| `code` | no | send your own 4–8 digit code instead of letting NetArz generate one (rarely needed) |
| `client_ip` | no | the end user's IP, for the delivery log |

Response `201`:

```json
{
  "id": "9b0a6c1e-2f55-4d7a-9a51-0c3d2b7e8f10",
  "object": "otp",
  "channel": "sms",
  "purpose": "login",
  "to": "0912***4567",
  "expires_at": "2026-09-29T10:02:00+03:30",
  "resend_after": 60,
  "message_id": "…"
}
```

What the user receives by SMS (one 70-character Persian part, the legally required
opt-out footer «لغو۱۱» is added by NetArz):

```
کد ورود YourSite: 483920
اعتبار: ۲ دقیقه
کد را به کسی ندهید.
لغو۱۱
```

By email, the letter carries **your** site's name and colour (the sender address is the
one `GET /account` reports), with
the code in a large box, its validity, and a "do not share" line.

### 2.2 Check a code — `POST /otp/verify`

```bash
curl -X POST https://netarz.ir/api/msg/v1/otp/verify \
  -H "Authorization: Bearer $NETARZ_MSG_SECRET" -H "Content-Type: application/json" \
  -d '{"id": "9b0a6c1e-…", "code": "483920"}'
```

Instead of `id` you may send `to` (+ optional `purpose`, default `login`) and NetArz
checks the newest live code for that recipient. Persian digits in `code` are accepted.

Success `200`:

```json
{ "verified": true, "id": "9b0a6c1e-…", "channel": "sms", "purpose": "login", "recipient": "09121234567", … }
```

Use the returned `recipient` (normalised: `09xxxxxxxxx` or lower-case email) as the
identity you sign in — not what the browser posted.

Failures (all JSON `{"error": {"code", "message", …}}`):

| HTTP | `error.code` | what to do |
|---|---|---|
| 422 | `invalid_code` | wrong code; `attempts_left` tells how many tries remain |
| 410 | `otp_expired` | ask the user to request a new code |
| 410 | `otp_used` | a code works exactly once |
| 429 | `too_many_attempts` | 5 wrong tries burnt this code; request a new one |
| 404 | `otp_not_found` | unknown id, or it belongs to another app |

Rules NetArz enforces so you do not have to:

- One code per recipient every **60 seconds**, at most **5 per hour** →
  `429 resend_too_soon` / `too_many_codes` with `retry_after` (seconds) and a `Retry-After` header.
  Show a countdown on the "send again" button from `resend_after`.
- A new code retires the recipient's earlier codes for the same purpose.
- Codes are stored only as a keyed hash; nobody at NetArz can read them back.

You still own: your own per-IP limit on the page that asks for a code (a captcha or a
simple throttle), and never telling the browser whether an account exists.

---

## 3. Transactional email — `POST /email`

For links (magic login, password reset, email confirmation), receipts and notices.

```bash
curl -X POST https://netarz.ir/api/msg/v1/email \
  -H "Authorization: Bearer $NETARZ_MSG_SECRET" -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "بازیابی رمز عبور",
    "text": "برای ساختن رمز تازه روی دکمهٔ زیر بزنید.\n\nاین لینک تا یک ساعت کار می‌کند.",
    "button_label": "ساختن رمز تازه",
    "button_url": "https://example.com/reset-password/abc123"
  }'
```

| field | required | meaning |
|---|---|---|
| `to` | yes | one address |
| `subject` | yes | up to 200 characters |
| `text` | one of text/html | plain paragraphs (blank line = new paragraph), laid out in a right-to-left letter with your name and colour |
| `html` | one of text/html | your own complete HTML, sent as-is (a plain-text part is derived from it) |
| `button_label`, `button_url` | no | one call-to-action button under `text`. **`button_url` must be on your registered domains**, or `422 url_not_allowed` |
| `reply_to` | no | where replies go |
| `purpose` | no | your own label for the log, e.g. `password_reset` |

Response `201` is a message object (`id`, `status: "sent"`); `502` with
`status: "failed"` and `error` if the mail server refused it. For a login **code** by
email, use `POST /otp` with `channel: "email"` — not this endpoint.

---

## 4. Transactional SMS — `POST /sms`

Only when the NetArz admin enabled free text for your app. For codes always use `/otp`.

```json
{ "to": "09121234567", "text": "نوبت شما فردا ساعت ۱۰ ثبت شد.", "purpose": "booking" }
```

- A Persian SMS is billed per 70 characters (67 per part after the first); the footer
  «لغو۱۱» counts. Up to 3 parts are accepted, beyond that `422 text_too_long`.
- **The current SMS line refuses any message containing a link — even a bare
  domain like `example.com`.** Name the page in words instead («بخش نوبت‌های من»).
  A link returns `422 links_not_allowed`.
- No advertising. Promotional SMS needs the recipient's consent and is not what this
  API is for.

---

## 5. Message status — `GET /messages/{id}`

Returns `{id, channel, kind, purpose, to (masked), status, error, parts, created_at}`
for a message your app sent. `status` is `sent` (handed to the operator / mail relay)
or `failed`.

---

## 6. Errors and limits

Every error is `{"error": {"code": "...", "message": "<Persian, safe to show>"}}`.

| HTTP | code | meaning |
|---|---|---|
| 401 | `missing_key`, `invalid_key` | fix the `Authorization` header |
| 403 | `app_suspended` | the admin paused your app |
| 403 | `channel_disabled`, `free_text_disabled` | this channel/endpoint is not enabled for you |
| 422 | `validation_failed` | `fields` lists what is wrong |
| 422 | `invalid_mobile`, `invalid_email` | bad recipient |
| 429 | `daily_cap_reached` | your app's daily cap for this channel is used up |
| 429 | `capacity_reserved` | the shared SMS capacity for today is used up; offer email instead |
| 429 | `resend_too_soon`, `too_many_codes` | per-recipient pacing; see `retry_after` |
| 502 | `delivery_failed` | the operator/mail relay refused; retry later or offer the other channel |

Request rate: 120 calls per minute per app.

A robust login page offers **both channels**: if SMS fails with `capacity_reserved` or
`delivery_failed`, offer "send the code by email" (and vice versa).

---

## 7. Laravel example

`.env`:

```
NETARZ_MSG_BASE=https://netarz.ir/api/msg/v1
NETARZ_MSG_SECRET=sk_msg_...
```

`config/services.php`:

```php
'netarz_msg' => [
    'base' => env('NETARZ_MSG_BASE', 'https://netarz.ir/api/msg/v1'),
    'secret' => env('NETARZ_MSG_SECRET'),
],
```

`app/Services/NetarzMessaging.php`:

```php
namespace App\Services;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

class NetarzMessaging
{
    private function http()
    {
        return Http::baseUrl(config('services.netarz_msg.base'))
            ->withToken(config('services.netarz_msg.secret'))
            ->acceptJson()->asJson()->timeout(20);
    }

    /** @return array{ok: bool, id?: string, resend_after?: int, error?: string, message?: string, retry_after?: int} */
    public function sendCode(string $channel, string $to, string $purpose = 'login'): array
    {
        $r = $this->http()->post('/otp', [
            'channel' => $channel, 'to' => $to, 'purpose' => $purpose,
            'client_ip' => request()->ip(),
        ]);

        return $r->successful()
            ? ['ok' => true, 'id' => $r['id'], 'resend_after' => $r['resend_after']]
            : $this->failure($r);
    }

    /** @return array{ok: bool, recipient?: string, error?: string, message?: string, attempts_left?: int} */
    public function checkCode(string $id, string $code): array
    {
        $r = $this->http()->post('/otp/verify', ['id' => $id, 'code' => $code]);

        return $r->successful() && $r['verified'] === true
            ? ['ok' => true, 'recipient' => $r['recipient']]
            : $this->failure($r);
    }

    public function email(string $to, string $subject, string $text, ?string $buttonLabel = null, ?string $buttonUrl = null): bool
    {
        return $this->http()->post('/email', array_filter([
            'to' => $to, 'subject' => $subject, 'text' => $text,
            'button_label' => $buttonLabel, 'button_url' => $buttonUrl,
        ]))->successful();
    }

    private function failure(Response $r): array
    {
        return ['ok' => false] + (array) ($r->json('error') ?? ['code' => 'network', 'message' => 'ارتباط با سرویس پیام برقرار نشد.']);
    }
}
```

In the login controller: on "send code" store `session(['otp_id' => $result['id']])`;
on "check code" call `checkCode(session('otp_id'), $request->code)`, then find or
create the user by `$result['recipient']` and `Auth::login()` them, and
`session()->forget('otp_id')`.

To route Laravel's own password-reset and verify-email notifications through NetArz,
override `sendPasswordResetNotification()` / `sendEmailVerificationNotification()` on
the User model and call `email()` with the URL Laravel built — or keep Laravel's
`Mail` and add a custom mail transport; the first is simpler.

---

## 8. Checklist

- [ ] The key lives only in the server's environment, never in the repository or the browser.
- [ ] Codes are sent and checked only through `/otp` and `/otp/verify`; your DB stores no code.
- [ ] The identity you sign in comes from `recipient` in the verify response.
- [ ] The "send again" button waits `resend_after` seconds; 429 responses show their message.
- [ ] The code page accepts Persian digits and `autocomplete="one-time-code"`, `inputmode="numeric"`.
- [ ] Every error `message` is shown as-is; it is written for end users in Persian.
- [ ] Email buttons point only to your own registered domains.
- [ ] If one channel fails, the user is offered the other.
