---
feature: Authentication
slug: auth
surface: provider
consumer: flutter
version: 2.2.0
status: ready
brd_version: "1.3"
brd_ids: [PROV-001, PROV-005]
business_rules: [BR-01, BR-02, BR-03]
decisions: [TD-01, DEC-15, DEC-17, DEC-18, DEC-23, TD-13, TD-17, TD-19, TD-20, TD-22, TD-23, TD-24, TD-25, TD-26, TD-29, TD-30]
schema_tables: [users, verifications, devices, device_apps, providers, provider_subscriptions, featured_plans, featured_plan_translations]
screens: ["provider--auth--*", "provider--registration--0[1-2]-*"]
related_docs: [../admin/auth.md, ../client/auth.md, ../provider/auth.md, ../delivery/auth.md]
last_updated: 2026-11-08
---

# Authentication — Flutter Integration Guide (provider)

Base URL: {HOST}/api/v1/provider. Shared headers, Dio setup and envelope:
[API_CONVENTIONS.md](../API_CONVENTIONS.md). Responses below are captured from
passing MySQL feature tests; JWTs are redacted. Initial published version is
1.0.0; migration from the undocumented client base is a MAJOR change.

## Version history

| Version | Date | Type | Summary | App action required |
|---|---|---|---|---|
| 2.2.0 | 2026-11-08 | MINOR | Consent texts are readable before login through `GET /provider/pages/{slug}` (TD-175). | Link the consent checkboxes to `GET /pages/{slug}`. |
| 2.1.0 | 2026-10-21 | MINOR | Session policy (7-day access token, refresh after expiry for 60 days), push token refresh endpoint, per-app OTP send limit, localized messages, consent screen changes; `full_name` optional at registration | Refresh on 401 before signing out; register the push token with `POST notifications/device`; add consent controls (§2) |
| 2.0.1 | 2026-10-20 | PATCH | Per-IP auth limits raised for carrier CGNAT (TD-26); no contract change | None |
| 2.0.0 | 2026-10-20 | MAJOR | Temporary fixed four-digit OTP 1234 until gateway integration (TD-24) | Accept four digits and use 1234 for verification |
| 1.0.0 | 2026-10-20 | Initial | Phone OTP and app-scoped sessions | Send phone/consents; read access_token and user/profile |

## Changelog

### 2.2.0 — 2026-11-08

- **Added:** Pointer to the new public pages endpoints for the terms, privacy and female declaration consent labels. No contract change in this doc.
- **Breaking:** No. **App action required:** Link the consent checkboxes to `GET /pages/{slug}`.

### 2.1.0 — 2026-10-21

- **Changed (TD-29):** access tokens now last 7 days (`expires_in` = 604800, was 5184000). `auth/refresh` accepts an **expired** token for 60 days after the original sign-in, keeps the `app` claim and revokes the old token; after that window it returns 401.
- **Changed (TD-26 amendment):** the OTP send limit is 5 per phone **per app** per 10 minutes, with a ceiling of 12 per phone across the three apps. Per-IP limits unchanged.
- **Changed (TD-23 amendment, non-breaking):** `full_name` is optional at registration. Step 1 has one name field: send it as `display_name` (the app may also send it as `full_name`). A new identity takes `display_name` as its name when `full_name` is missing; an existing identity's name is never overwritten.
- **Added:** `POST /provider/notifications/device` to refresh the FCM token after login (allowed before approval).
- **Changed (DEC-17):** validation, 401, 403, 404, 405, 429 and 500 messages are Arabic by default and English with `Accept-Language: en`. A 422 message carries a translated "(and N more errors)" suffix. A 500 returns `data.error_code = INTERNAL_ERROR` and `data.correlation_id`, never SQL or exception text.
- **Docs (DEC-18):** §2 lists the consent controls each screen must add. The app must never send `true` without a user action.
- **Breaking:** No for the documented endpoints.
- **App action required:** on a 401, call `auth/refresh` once with the stored token before signing the user out; after every login and on FCM `onTokenRefresh`, call `POST notifications/device`; add the consent controls in §2.
- **BRD / decisions:** TD-29, TD-26, TD-23, TD-30, DEC-17, DEC-18

### 2.0.1 — 2026-10-20
- **Changed:** per-IP limits raised: otp-send 30 → 300 per IP per 10 minutes, otp-verify 60 → 300 per IP per minute. Per-phone and per-token limits unchanged. All values are env-configurable (`AUTH_*` in `.env`).
- **Internal (no response change):** new identities are stored with `users.user_type = client` ("app user") whichever app they join first (TD-25); auth resources no longer query the database.
- **Breaking:** No
- **App action required:** None
- **BRD / decisions:** TD-25, TD-26

### 2.0.0 — 2026-10-20

- **Changed:** shared OTP generator returns the fixed four-digit string 1234 for send/resend while gateway integration is pending.
- **Breaking:** Yes; verification now requires four digits instead of six.
- **Endpoints affected:** auth/register, auth/login, auth/verify, auth/resend-code.
- **App action required:** accept four-digit text and submit 1234 with the returned verification_token.
- **Decision:** TD-24 temporarily supersedes the six-digit generation policy in DEC-15; SMS channel still logs. Expiry, attempts, throttling and app isolation continue to apply.


### 1.0.0 — 2026-10-20

- **Added:** register/login/verify/resend-code, refresh/logout/me, per-profile consents, app isolation.
- **Changed:** identity fields under data.user, app fields under data.profile.
- **Fixed:** random configured OTP replaces fixed 1111 shortcut.
- **Removed / Deprecated:** N/A — first surface implementation; no password endpoints.
- **Breaking:** No prior surface API.
- **Endpoints affected:** all seven endpoints in §3.
- **App action required:** use phone flow; keep challenge token; store this app JWT separately.
- **BRD / decisions:** PROV-001, PROV-005; DEC-15 supersedes BRD password flows.

## 1. Business summary

One phone identifies one person across apps. Registration requires three explicit
consents. Verification creates this app profile transactionally and activates
phone authentication. Login requires this app profile to exist already.
New profile status is incomplete; review status remains readable, while operations require approved.

## 2. User flow and screens

| Step | Screen/widget | Endpoint | States |
|---|---|---|---|
| 1 | Welcome → login or registration | POST auth/login or auth/register | Loading, translated error, 404 sign-up / 409 login branch |
| 2 | OTP | POST auth/verify or auth/resend-code | Four digits (1234), cooldown countdown, incorrect/expired/locked code |
| 3 | Onboarding / review status | GET auth/me | Current profile, loading and retry |

Screens: provider--auth--*, provider--registration--0[1-2]-*. TD-24 temporarily sets a fixed four-digit OTP, 1234, until the gateway is integrated.

Establishment is one account (D4). Step 1 has **one** name field, so the API
needs only `display_name` (TD-23 amendment, 2026-10-21):

| Screen | Name field on screen | What the app sends |
|---|---|---|
| `provider--registration--01-basic-individual` | Her name | `provider_type: "individual"`, `display_name: <name>`; may also send the same value as `full_name` |
| `provider--registration--02-basic-establishment` | Establishment name | `provider_type: "establishment"`, `display_name: <establishment name>`; no `full_name` (the screen does not collect the person's name) |

A **new** identity without `full_name` takes `display_name` as its account
name (`user.full_name`). A person who already uses another Joshen app keeps
her existing account name; `display_name` is stored only on the provider
profile (TD-23).

### Screen changes required

The API requires explicit consent (BR-01, DEC-18, TD-17). The prototype
screens below do not collect all of it yet. Add these controls; each
checkbox starts unchecked and the request field is `true` only after the user
ticks it — **never send a hard-coded `true`**. "Send code" stays disabled until
all three are ticked. Open item for product/design (DECISIONS resolution log,
2026-10-21).

| Screen | Control to add | Request field |
|---|---|---|
| `provider--registration--01-basic-individual` | **add** a female declaration checkbox before "Send code" | `guarantees_female_gender` |
| `provider--registration--01-basic-individual` | **add** terms and privacy checkboxes (two separate controls) before "Send code" | `accept_terms`, `accept_policies` |
| `provider--registration--02-basic-establishment` | **add** the same three checkboxes before "Send code" | `guarantees_female_gender`, `accept_terms`, `accept_policies` |

## 3. Endpoints

Headers: Accept and Content-Type application/json; optional Accept-Language ar/en
(default ar). Protected endpoints require Authorization: Bearer {access_token}
with app=provider and its profile; **approval is not required for auth endpoints**.
No path/query parameters. All successes are HTTP 200. Common errors follow §3.7.

### 3.1 Register — POST /api/v1/provider/auth/register

Public; otp-send limiter (config/auth_limits.php), per 10 minutes: 5 per phone
in this app, 12 per phone across the three apps, and 300 per IP (high on
purpose: carrier CGNAT, TD-26). A separate per-phone/per-app cooldown is
60 seconds for send/resend.

| Field | Type | Required | Validation |
|---|---|---|---|
| full_name | string/null | no | max 255; defaults to display_name for a new identity |
| phone_code | string | yes | 966 / 00966 / +966 normalize to +966; Saudi only |
| phone | string | yes | 5XXXXXXXX or 05XXXXXXXX normalize without leading zero |
| guarantees_female_gender | accepted | yes | Explicit accepted value |
| accept_policies | accepted | yes | Explicit accepted value |
| accept_terms | accepted | yes | Explicit accepted value |
| provider_type | string | yes | individual/establishment |
| display_name | string | yes | max 150; the step-1 name field (personal or establishment name) |

Accepted values: true, 1, '1', 'true', 'yes', 'on'. Password, password_confirmation
and email fields are prohibited on register/login. Other unknown fields are ignored.

~~~json
{
  "full_name": "Test Account",
  "phone_code": "966",
  "phone": "0512345678",
  "accept_terms": true,
  "accept_policies": true,
  "guarantees_female_gender": true,
  "provider_type": "individual",
  "display_name": "Test Provider"
}
~~~
~~~json
{
    "status": "success",
    "message": "Verification code sent.",
    "data": {
        "verification_token": "01a15951-ae10-7389-af34-6a11e09a6e40",
        "expires_at": "2026-10-20T12:48:07+00:00",
        "resend_available_at": "2026-10-20T12:39:07+00:00"
    }
}
~~~

The opaque verification_token belongs to this app. expires_at is code expiry;
resend_available_at is the earliest resend. Both are ISO 8601 with offset.
No account/profile is written until verify. Pending input is encrypted in cache.
A new challenge invalidates the previous challenge for this phone/app.

### 3.2 Login — POST /api/v1/provider/auth/login

Public; same send limiter/cooldown. Only phone_code and phone are needed:

~~~json
{
  "phone_code": "966",
  "phone": "0512345678"
}
~~~
~~~json
{
    "status": "success",
    "message": "Verification code sent.",
    "data": {
        "verification_token": "01a15951-aec0-707e-b73e-240cbf7a863e",
        "expires_at": "2026-10-20T12:48:07+00:00",
        "resend_available_at": "2026-10-20T12:39:07+00:00"
    }
}
~~~

A profile in another app yields 404 here. A banned/disabled account gets 403.
Deleted identities count as unregistered. Joining another app preserves the
existing account name and locale; it creates only the new profile at verify.

### 3.3 Verify — POST /api/v1/provider/auth/verify

Public; otp-verify limiter: 10 per verification token and 300 per IP per minute.

| Field | Type | Required | Validation |
|---|---|---|---|
| verification_token | UUID string | yes | Challenge from this app |
| code | string | yes | Configured digit length; currently exactly 4 ASCII digits; temporary code 1234 |
| device_token | string/null | no | max 4096 |
| device_type | string/null | with device_token | ios/android/huawei |
| app_version | string/null | no | max 20 |
| password | prohibited | — | No app-user password |

Request shape (substitute the current challenge token and received SMS code):
~~~json
{"verification_token":"{{verification_token}}","code":"{{otp_code}}","device_token":"replace-with-push-token","device_type":"ios","app_version":"1.0.0"}
~~~

Send the token returned by register/login and the code received by SMS as text.
Do not parse it as a number: leading zeroes matter. Optional device fields
register this app for push. Same push token in different apps has separate
device records because device_apps.device_id is unique (TD-23).

~~~json
{
    "status": "success",
    "message": "Phone verified successfully.",
    "data": {
        "access_token": "[redacted JWT]",
        "token_type": "bearer",
        "expires_in": 604800,
        "user": {
            "id": "01a15d78-ea18-7122-98d3-18533a79be10",
            "full_name": "Test Account",
            "phone_code": "+966",
            "phone": "512345678",
            "email": null,
            "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
            "locale": "en"
        },
        "profile": {
            "id": "01a15d78-ea1a-7232-81ad-897d9f943320",
            "provider_type": {
                "value": "individual",
                "label": "Individual"
            },
            "display_name": "Test Provider",
            "account_status": {
                "value": "incomplete",
                "label": "Incomplete"
            },
            "status_reason": null,
            "is_accepting_bookings": null,
            "instant_enabled": null,
            "is_subscribed": false,
            "subscription_plan": null
        }
    }
}
~~~

access_token is the JWT; token_type=bearer; expires_in is the access-token lifetime in seconds: 604800 (7 days, TD-29).
user.id and profile.id are different UUIDs. Stable user fields: id, full_name,
phone_code, phone, nullable email, image URL/placeholder, locale.
Every verification sets last_login_at and phone_verified_at; historical phone formats are canonicalized. An identity deleted after send cannot consume that older challenge.
Five incorrect codes consume the allowance; the sixth attempt returns 429.
Codes expire after ten minutes; all three values come from Verification config.
Consumed challenges cannot issue a second token.

### 3.4 Resend — POST /api/v1/provider/auth/resend-code

Public; same otp-send limits and 60-second cooldown. Body contains only
verification_token. Send the token from the active flow, not a phone/code.

~~~json
{
  "verification_token": "01a15951-af52-71f2-b052-f8bef1c37e14"
}
~~~
~~~json
{
    "status": "success",
    "message": "Verification code sent.",
    "data": {
        "verification_token": "01a15951-af52-71f2-b052-f8bef1c37e14",
        "expires_at": "2026-10-20T12:49:08+00:00",
        "resend_available_at": "2026-10-20T12:40:08+00:00"
    }
}
~~~

Same token and fixed code 1234, renewed expiry, attempts reset to zero. Pending context
lasts 24 hours so an expired code can be resent; after context expiry restart
the phone flow. Consumed/other-app challenges return OTP_EXPIRED.

### 3.5 Refresh — POST /api/v1/provider/auth/refresh

Send the stored token as `Authorization: Bearer …`, empty body. The token may
be **expired**: refresh works for 60 days after the original sign-in (the
first `verify`), not after the last refresh (TD-29). It returns a new token
(same `app` claim, `expires_in` 604800) plus the current user/profile and
revokes the old token. Save the new token.

| Case | Result |
|---|---|
| Valid or expired token, within 60 days of sign-in | 200, new token |
| Token already refreshed or logged out | 401 UNAUTHENTICATED |
| More than 60 days after sign-in, missing or invalid token | 401 UNAUTHENTICATED → sign in again |
| Token from another app | 403 PERMISSION_DENIED |
| Account banned or disabled | 403 ACCOUNT_INACTIVE |

Response after the access token expired (real capture):

~~~json
{
    "status": "success",
    "message": "Token refreshed.",
    "data": {
        "access_token": "[redacted JWT]",
        "token_type": "bearer",
        "expires_in": 604800,
        "user": {
            "id": "01a15d78-ed46-738c-873a-bce32df27067",
            "full_name": "Test Account",
            "phone_code": "+966",
            "phone": "512345678",
            "email": null,
            "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
            "locale": "en"
        },
        "profile": {
            "id": "01a15d78-ed47-73ec-bf6e-cb5ea556e5c3",
            "provider_type": {
                "value": "individual",
                "label": "Individual"
            },
            "display_name": "Test Provider",
            "account_status": {
                "value": "incomplete",
                "label": "Incomplete"
            },
            "status_reason": null,
            "is_accepting_bookings": false,
            "instant_enabled": false,
            "is_subscribed": false,
            "subscription_plan": null
        }
    }
}
~~~

### 3.6 Logout — POST /api/v1/provider/auth/logout

Protected; optional device_token string (max 4096). Revokes JWT and removes
only this app association for that push token. Without device_token, only the
session is revoked. Success data is null.

~~~json
{
  "device_token": "test-push-token"
}
~~~
~~~json
{
    "status": "success",
    "message": "You logged out successfully.",
    "data": null
}
~~~

### 3.7 Me — GET /api/v1/provider/auth/me

Protected, including incomplete/pending-review profiles. No body. Same user
and profile as verify, without token fields. Stable nullable keys remain present.

~~~json
{
    "status": "success",
    "message": "Account retrieved successfully.",
    "data": {
        "user": {
            "id": "01a15951-ae21-7256-bbd5-93f5e39b28c6",
            "full_name": "Test Account",
            "phone_code": "+966",
            "phone": "512345678",
            "email": null,
            "image": "https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200",
            "locale": "en"
        },
        "profile": {
            "id": "01a15951-ae24-702e-b1ce-c9655173f7d2",
            "provider_type": {
                "value": "individual",
                "label": "Individual"
            },
            "display_name": "Test Provider",
            "account_status": {
                "value": "incomplete",
                "label": "Incomplete"
            },
            "status_reason": null,
            "is_accepting_bookings": false,
            "instant_enabled": false,
            "is_subscribed": false,
            "subscription_plan": null
        }
    }
}
~~~

### 3.8 Push token — POST /api/v1/provider/notifications/device

Protected (this app's token; approval not required). Call it after every login
and whenever FCM rotates the token. It upserts the same `devices` +
`device_apps` registration as verify, bound to the `provider` app (TD-23): one row
per account, app and token; a repeated call only updates `app_version` and
`last_seen_at`.

| Field | Type | Required | Validation |
|---|---|---|---|
| device_token | string | yes | max 4096 |
| device_type | string | yes | ios / android / huawei |
| app_version | string/null | no | max 20 |
| device_id | — | prohibited | old base field; 422 when sent |

~~~json
{"device_token": "fcm-rotated", "device_type": "android", "app_version": "2.0.0"}
~~~
~~~json
{
    "status": "success",
    "message": "Device registered.",
    "data": null
}
~~~

Errors: 422 (missing `device_type`, `device_id` sent), 401, 403 (other app).

### Errors shared by the endpoints

| HTTP | data.error_code | Trigger | App action |
|---|---|---|---|
| 422 | — | Invalid request shape, including missing/false consent. `message` = first error + "(and N more errors)", localized; `data` is null | Correct input; show message |
| 404 | ACCOUNT_NOT_REGISTERED | Login without this app profile; deleted identity | Open sign-up |
| 409 | ENTITY_ALREADY_EXISTS | Registration with this app profile | Open login |
| 422 | INVALID_OTP | Incorrect code; increments persisted counter | Correct code |
| 422 | OTP_EXPIRED | Unknown, expired, consumed or other-app challenge | Restart phone flow |
| 429 | OTP_TOO_MANY_ATTEMPTS | Attempt allowance exhausted | Resend after cooldown |
| 429 | RATE_LIMITED | Cooldown or named limiter | Respect Retry-After when present, otherwise resend timer |
| 403 | ACCOUNT_INACTIVE | Banned, disabled, inactive account/role, or approval gate | Contact support; review gate includes data.account_status |
| 403 | PERMISSION_DENIED | Missing/different app claim or missing profile | Use this app login |
| 401 | UNAUTHENTICATED | Missing, expired, invalid or revoked JWT | Call auth/refresh once; if that is also 401, clear this app session |
| 500 | INTERNAL_ERROR | Unexpected server failure; `data.correlation_id` set, no internals | Show a generic error; report the correlation id |
| 503 | INFRASTRUCTURE_FAILURE | SMS abstraction/enqueue failure | Retry later |

All responses use {status,message,data}. Domain failures use status=fail and
data.error_code; validation failures use `data.errors` (TD-02). Never branch
on translated messages. OTPs never appear in HTTP responses.

### 3.x Consent texts (TD-175)

The terms, privacy policy and female-only declaration the user ticks at registration are readable without a token: `GET /provider/pages/terms`, `GET /provider/pages/privacy` and `GET /provider/pages/female_declaration` ([provider/content.md](content.md)). Open them from the consent labels; `version` in the response tells the app which text the user was shown.

## 4. Statuses and enums

| account_status.value | Meaning | Account/auth access | Operations |
|---|---|---|---|
| incomplete | Onboarding required | yes | no |
| pending_review | Awaiting review | yes | no |
| needs_changes | Corrections required | yes | no |
| approved | Reviewed | yes | yes |
| rejected | Rejected registration | yes | no |
| suspended | Operations suspended | yes | no |

Statuses are {value,label}, localized to Arabic/English. On operational denial,
data.account_status is the raw machine string. status_reason is nullable text.
Auth creates incomplete; later onboarding/review features own transitions.
No can_* flags in auth.

provider_type is individual/establishment. is_accepting_bookings and instant_enabled
are booleans. is_subscribed requires active status and starts_at <= now < ends_at.
subscription_plan is null or {id,name,billing_period:{value,label},ends_at}.
Billing period is monthly/yearly; ends_at is ISO 8601. Plan never implies Verified.

## Data model

| Table | Read/write | Use |
|---|---|---|
| users | read/write | Identity, challenge, app profile, wallet or push registration |
| verifications | read/write | Identity, challenge, app profile, wallet or push registration |
| devices | read/write | Identity, challenge, app profile, wallet or push registration |
| device_apps | read/write | Identity, challenge, app profile, wallet or push registration |
| providers | read/write | Identity, challenge, app profile, wallet or push registration |
| provider_subscriptions | read | Plan projection |
| featured_plans | read | Plan projection |
| featured_plan_translations | read | Plan projection |

## 5. Dart models

~~~dart
class AuthUser {
  final String id, fullName, phoneCode, phone, locale;
  final String? email, image;
  AuthUser.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String, fullName = j['full_name'] as String,
        phoneCode = j['phone_code'] as String, phone = j['phone'] as String,
        email = j['email'] as String?, image = j['image'] as String?,
        locale = j['locale'] as String;
}
class OtpChallenge {
  final String verificationToken;
  final DateTime expiresAt, resendAvailableAt;
  OtpChallenge.fromJson(Map<String, dynamic> j)
      : verificationToken = j['verification_token'] as String,
        expiresAt = DateTime.parse(j['expires_at'] as String),
        resendAvailableAt = DateTime.parse(j['resend_available_at'] as String);
}
class EnumValue {
  final String value, label;
  EnumValue.fromJson(Map<String, dynamic> j)
      : value = j['value'] as String, label = j['label'] as String;
}
class SubscriptionPlan {
  final String id, name;
  final EnumValue billingPeriod;
  final DateTime endsAt;
  SubscriptionPlan.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String, name = j['name'] as String,
        billingPeriod = EnumValue.fromJson(j['billing_period'] as Map<String, dynamic>),
        endsAt = DateTime.parse(j['ends_at'] as String);
}
class ProviderProfile {
  final String id, displayName;
  final EnumValue providerType, accountStatus;
  final String? statusReason;
  final bool isAcceptingBookings, instantEnabled, isSubscribed;
  final SubscriptionPlan? subscriptionPlan;
  ProviderProfile.fromJson(Map<String, dynamic> j)
      : id = j['id'] as String, displayName = j['display_name'] as String,
        providerType = EnumValue.fromJson(j['provider_type'] as Map<String, dynamic>),
        accountStatus = EnumValue.fromJson(j['account_status'] as Map<String, dynamic>),
        statusReason = j['status_reason'] as String?,
        isAcceptingBookings = j['is_accepting_bookings'] as bool,
        instantEnabled = j['instant_enabled'] as bool,
        isSubscribed = j['is_subscribed'] as bool,
        subscriptionPlan = j['subscription_plan'] == null ? null
          : SubscriptionPlan.fromJson(j['subscription_plan'] as Map<String, dynamic>);
}

class AuthSession {
  final String? accessToken, tokenType;
  final int? expiresIn;
  final AuthUser user;
  final ProviderProfile profile;
  AuthSession.fromJson(Map<String, dynamic> j)
      : accessToken = j['access_token'] as String?,
        tokenType = j['token_type'] as String?, expiresIn = j['expires_in'] as int?,
        user = AuthUser.fromJson(j['user'] as Map<String, dynamic>),
        profile = ProviderProfile.fromJson(j['profile'] as Map<String, dynamic>);
}
~~~

## 6. Business rules enforced by the backend

| Source | Enforcement | User behavior |
|---|---|---|
| BR-01 / TD-17 / DEC-18 | Three accepted consents; per-profile timestamps | Explicit acceptance |
| TD-01 / DEC-15 | One phone identity, per-app profile and JWT claim | Separate app sign-in |
| BR-02 | Guests excluded from account routes | Save attempted action, resume after login |
| BR-03 | Provider/driver operations require approved | Read status from me |
| TD-19/20/22 | Public account/profile UUIDs; private integer FKs | Keep IDs as strings |
| DEC-17 | Arabic default, English supported | Display translated labels |

## 7. Notifications and real-time events

| Event | Channel | Payload | App action |
|---|---|---|---|
| Challenge send/resend | SMS channel, currently logs | Configured code, never HTTP response | Enter SMS code |
| Verification completed | Internal queued package event | No public realtime contract | Continue from HTTP response |

Temporary OTP is 1234 for register, login and resend (TD-24).

No auth push/socket contract. Existing base welcome jobs are infrastructure;
the future notification feature owns event templates.

## 8. Localization and UI notes

Default Arabic; request English via Accept-Language: en. New/pending identity
stores registration locale. Joining another app preserves account name/locale.
Use RTL for Arabic; phone/code controls LTR. Show enum label, branch on value.
Parse ISO dates and display Asia/Riyadh; money remains a 2-decimal string.

Screen changes required (labels for the consent controls in §2):

| Request field | Arabic label | English label |
|---|---|---|
| `guarantees_female_gender` | أقر بأنني أنثى | I confirm I am female |
| `accept_terms` | أوافق على الشروط والأحكام | I accept the terms and conditions |
| `accept_policies` | أوافق على سياسة الخصوصية | I accept the privacy policy |

Every error message (validation 422, 401, 403, 404, 405, 429, 500) follows
`Accept-Language` and is Arabic when the header is missing. A 422 message is
the first error plus a translated count of the others:
`حقل رمز الدولة مطلوب. (و 1 خطأ آخر)` / `The phone code field is required. (and 1 more error)`.
Show `message` as-is.

## 9. Integration checklist

- [ ] Use this prefix; store app tokens separately
- [ ] Supply all registration fields and explicit consents
- [ ] Four-digit OTP stays text; no password controls
- [ ] Parse stable nullable fields, UUID strings and money strings
- [ ] Handle every error code and 404/409 login/sign-up branch
- [ ] Disable double submits; implement resend countdown and loading/retry states
- [ ] On 401, refresh once with the stored (possibly expired) token; clear the session only if refresh also fails
- [ ] Rotate JWT on refresh; clear after logout
- [ ] Call `POST notifications/device` after login and on FCM token refresh
- [ ] Consent checkboxes start unchecked; never send a hard-coded `true` (§2)
- [ ] Send device token/type together; detach on logout
- [ ] Show review status from me and gate operational UI until approved
- [ ] Verify Arabic RTL and English

## 10. Acceptance scenarios

| AC ID | Scenario | Expected |
|---|---|---|
| AC-PROV-001 | Provider registration | Incomplete profile; consent; operations denied |
| AC-PROV-005 | Login under review / approved | me allowed; operations approved only |

Evidence: tests/Feature/Api/V1/Provider/AuthControllerTest.php inherits
AppAuthContract; CrossAppAuthTest covers cross-app denial, multi-profile identity,
approval gates, shared-device logout and plan expiry. AC-CUST-003 password-reset
steps are intentionally not applicable under DEC-15.

## 11. Open decisions and known limitations

| ID | Impact | Current behavior |
|---|---|---|
| DEC-15 / TD-24 | Gateway will be provided later | Fixed OTP 1234; SMS channel still logs until integration |
| DEC-18 | Self-attested eligibility | Store declaration, no identity/gender verification service |
| TD-23 | Enumeration trade-off accepted for UX | Login unknown profile returns 404 |
| TD-23 | Pending state/phone lock in cache | Use a shared cache across API nodes; lost context restarts OTP |
| TD-29 | Session lengths are working values | Access 7 days, refresh window 60 days; final values pending product owner (env `AUTH_*_ACCESS_TTL` / `AUTH_*_REFRESH_TTL`) |
| DEC-18 | Consent screens | Prototype screens lack controls (§2); open for product/design |
| TD-25 | Notification inbox | Shared `GET /api/v1/notifications` (+ show, delete, mark-all-read, clear-all) returns this app's rows only — [API_CONVENTIONS §7](../API_CONVENTIONS.md#7-shared-in-app-notifications-all-apps). Templates/events arrive with the notifications feature |
| TD-23 | One app per device association | Separate device row per app, logout is app-scoped |
| DEC-23 | Featured-plan semantics | Expose active plan; never derive Verified badge from it |

Operational route groups are extension points; test-only probes exercise their
middleware. Onboarding, business operations and live gateway acceptance remain
separate features. No deployment or mobile-browser acceptance is claimed.
