# REST API Specification

**Base URL**: `https://api.yourdomain.com` (exposed via Cloudflare Tunnel)
**Authentication**: Bearer token (Laravel Sanctum)
**Format**: JSON for request/response

## Auth

All endpoints (except `/auth/*`) require header:

```
Authorization: Bearer {token}
```

Token can be obtained at `/auth/login` or `/auth/register`.

---

## Endpoints

### Authentication

#### POST `/auth/register`

Register a new user.

**Body**:
```json
{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "secret123",
  "password_confirmation": "secret123"
}
```

**Response** (201):
```json
{
  "user": {
    "id": 1,
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "email": "john@example.com",
    "name": "John Doe",
    "created_at": "2025-01-01T00:00:00Z"
  },
  "token": "eyJ..."`
}
```

**Errors**: 422 (validation), 409 (email exists)

---

#### POST `/auth/login`

Login user.

**Body**:
```json
{
  "email": "john@example.com",
  "password": "secret123"
}
```

**Response** (200):
```json
{
  "user": { ... },
  "token": "eyJ..."`
}
```

---

#### POST `/auth/logout`

Revoke current token.

**Headers**: `Authorization`

**Response** (204)

---

#### POST `/auth/forgot-password`

Request password reset email.

**Body**:
```json
{
  "email": "john@example.com"
}
```

**Response** (200): `{ "message": "Reset link sent" }`

---

#### POST `/auth/reset-password/{token}`

Reset password with token from email.

**Body**:
```json
{
  "email": "john@example.com",
  "password": "newpass123",
  "password_confirmation": "newpass123"
}
```

---

### User

#### GET `/user`

Get current user profile.

**Headers**: `Authorization`

**Response** (200):
```json
{
  "id": 1,
  "uuid": "...",
  "email": "john@example.com",
  "name": "John Doe",
  "phone": "+85212345678",
  "timezone": "Asia/Hong_Kong",
  "subscription": {
    "status": "active",
    "plan": "pro",
    "messages_used_this_month": 456,
    "messages_limit": 2000
  },
  "max_sessions": 3,
  "created_at": "..."
}
```

---

#### PUT `/user/profile`

Update profile (name, timezone, phone).

**Body**:
```json
{
  "name": "John Updated",
  "timezone": "Asia/Shanghai"
}
```

---

#### GET `/user/usage`

Get usage stats for current billing period.

**Response** (200):
```json
{
  "messages_used": 456,
  "messages_limit": 2000,
  "sessions_active": 2,
  "sessions_limit": 3,
  "period_start": "2025-01-01",
  "period_end": "2025-01-31"
}
```

---

### WhatsApp Sessions

#### GET `/sessions`

List all sessions for current user.

**Headers**: `Authorization`

**Response** (200):
```json
{
  "data": [
    {
      "id": 1,
      "uuid": "...",
      "name": "My iPhone",
      "phone_number": "+85212345678",
      "status": "connected",
      "last_connected_at": "2025-01-15T10:30:00Z",
      "created_at": "2025-01-01T00:00:00Z"
    }
  ]
}
```

---

#### POST `/sessions`

Create a new WhatsApp session (start OpenWA and generate QR).

**Headers**: `Authorization`

**Body**:
```json
{
  "name": "My iPhone"
}
```

**Response** (202 - accepted, not ready yet):
```json
{
  "session": {
    "id": 1,
    "uuid": "...",
    "name": "My iPhone",
    "status": "creating",
    "qr_code_url": null,
    "created_at": "..."
  },
  "message": "Session initializing. Poll /sessions/{id} for QR code."
}
```

**Notes**:
- Backend spawns OpenWA process with unique session ID
- Immediately returns; QR generation may take 5-10 seconds

---

#### GET `/sessions/{id}`

Get session details including QR if ready.

**Headers**: `Authorization`

**Response** (200):
```json
{
  "id": 1,
  "uuid": "...",
  "name": "My iPhone",
  "phone_number": "+85212345678",
  "status": "qr_ready",
  "qr_code_url": "https://api.yourdomain.com/sessions/1/qr",
  "last_connected_at": null,
  "created_at": "..."
}
```

If QR not ready, `qr_code_url` is `null`. Poll every 2 seconds until status is `qr_ready` or `connected`.

---

#### GET `/sessions/{id}/qr`

Returns image/png of QR code. Direct access (no JSON) after authorized.

**Headers**: `Authorization`

**Response**: PNG image (200) or 404 if not ready.

---

#### DELETE `/sessions/{id}`

Delete a session and stop OpenWA process.

**Headers**: `Authorization`

**Response** (204)

---

#### GET `/sessions/{id}/health`

Check real-time health of this session (last seen, recent errors).

**Response** (200):
```json
{
  "status": "connected",
  "last_seen": "2025-01-15T10:30:00Z",
  "uptime_seconds": 86400,
  "messages_sent_today": 42,
  "errors_last_24h": 0
}
```

---

### Contacts

#### GET `/contacts`

List contacts for current user. Supports pagination and filtering by tags.

**Query**:
- `page` (default 1)
- `per_page` (default 50)
- `search` (name or phone)
- `tag` (comma-separated)

**Response** (200):
```json
{
  "data": [
    {
      "id": 1,
      "uuid": "...",
      "name": "Alice",
      "phone": "+85212345678",
      "email": "alice@example.com",
      "tags": ["customer","vip"],
      "consent_status": "granted",
      "opted_out_at": null
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 50,
    "total": 120
  }
}
```

---

#### POST `/contacts`

Create a single contact manually.

**Body**:
```json
{
  "name": "Bob",
  "phone": "+85287654321",
  "email": "bob@example.com",
  "tags": ["lead"]
}
```

**Response** (201): contact object

---

#### PUT `/contacts/{id}`

Update contact.

**Body**:
```json
{
  "name": "Bob Updated",
  "tags": ["lead","contacted"]
}
```

---

#### DELETE `/contacts/{id}`

Delete contact.

---

#### POST `/contacts/import`

Bulk import contacts from CSV.

**Headers**: `Authorization`
**Content-Type**: `multipart/form-data`

**Form fields**:
- `file` (CSV file)
- `name_column` (e.g., "Name") - column mapping
- `phone_column` (e.g., "Phone")
- `email_column` (optional)
- `tag` (optional single tag to apply to all imported)
- `consent_granted` (boolean) - if user confirms consent was obtained

**Response** (202):
```json
{
  "job_id": "abc123",
  "message": "Import started. Check /contacts/imports/{job_id} for progress."
}
```

**Notes**:
- Large files processed via queued job
- Deduplication on `(user_id, phone)` - existing contacts are updated (merge tags) rather than duplicated
- Returns summary at end: imported, updated, skipped, errors

---

#### GET `/contacts/imports/{jobId}`

Check import job status.

**Response** (200):
```json
{
  "job_id": "abc123",
  "status": "processing|completed|failed",
  "total": 1000,
  "processed": 750,
  "imported": 700,
  "updated": 50,
  "skipped": 0,
  "errors": 0,
  "error_message": null
}
```

---

### Campaigns

#### GET `/campaigns`

List campaigns for user.

**Query**:
- `page`
- `per_page`
- `status` (filter)

**Response** (200):
```json
{
  "data": [
    {
      "id": 1,
      "uuid": "...",
      "name": "New Year Promotion",
      "wa_session_name": "My iPhone",
      "message_type": "text",
      "message_body": "Happy New Year! {name}",
      "schedule_type": "specific",
      "scheduled_at": "2025-01-01T00:00:00Z",
      "status": "completed",
      "total_recipients": 120,
      "messages_sent": 120,
      "messages_failed": 3,
      "started_at": "2025-01-01T00:00:01Z",
      "completed_at": "2025-01-01T00:05:00Z"
    }
  ],
  "meta": { ... }
}
```

---

#### POST `/campaigns`

Create a new campaign (draft).

**Body**:
```json
{
  "name": "Sale Alert",
  "wa_session_id": 1,
  "message_type": "text",
  "message_body": "Hi {name}, 20% off everything today!",
  "media_url": null,
  "schedule_type": "now",
  "scheduled_at": null,
  "contact_ids": [1,2,3,4]  // or use contact_list_id for saved lists
}
```

**Validation**:
- `wa_session_id` must exist and be `connected` or `qr_ready` (if now, must be `connected`)
- `message_body` required for text
- `media_url` required for image/document, plus optional caption
- `scheduled_at` must be future if schedule_type=specific
- `contact_ids` must be non-empty array of valid contact IDs belonging to user

**Response** (201): campaign object with status `draft`

---

#### GET `/campaigns/{id}`

Get campaign details including recipient list (paginated).

**Response**:
```json
{
  "id": 1,
  "uuid": "...",
  "name": "...",
  "message_type": "text",
  "message_body": "...",
  "schedule_type": "specific",
  "scheduled_at": "...",
  "status": "completed",
  "total_recipients": 120,
  "messages_sent": 120,
  "messages_failed": 3,
  "started_at": "...",
  "completed_at": "...",
  "recipients": {
    "data": [
      {
        "contact": { "name": "Alice", "phone": "+852..." },
        "status": "sent",
        "sent_at": "...",
        "delivered_at": "...",
        "failure_reason": null
      }
    ],
    "meta": { ... }
  }
}
```

---

#### POST `/campaigns/{id}/send`

Trigger campaign immediately (if status=draft) or resume (if paused).

**Headers**: `Authorization`

**Response** (202):
```json
{
  "message": "Campaign started",
  "campaign_id": 1
}
```

**Action**:
- If draft: set status `scheduled`, then dispatch `SendCampaignJob` with delay 0
- If paused: set status `scheduled`, requeue failed messages, dispatch job

---

#### POST `/campaigns/{id}/pause`

Pause a running campaign (stop sending more messages, but keep already-sent as is).

**Response** (200): `{ "status": "paused" }`

---

#### DELETE `/campaigns/{id}`

Delete campaign (only if draft or completed). Running campaigns cannot be deleted.

**Response** (204)

---

#### GET `/campaigns/{id}/report`

Get summary statistics for a campaign.

**Response** (200):
```json
{
  "campaign_id": 1,
  "name": "...",
  "status": "completed",
  "started_at": "...",
  "completed_at": "...",
  "duration_seconds": 300,
  "total": 120,
  "sent": 117,
  "delivered": 115,
  "read": 80,
  "failed": 3,
  "delivery_rate": 97.5,
  "read_rate": 69.5
}
```

---

### Billing

#### GET `/billing/plans`

List available subscription plans.

**Response** (200):
```json
{
  "plans": [
    {
      "id": "price_free",
      "name": "Free",
      "price_monthly": 0,
      "messages_per_month": 50,
      "max_sessions": 1,
      "features": ["text_only", "basic_support"]
    },
    {
      "id": "price_pro",
      "name": "Pro",
      "price_monthly": 15,
      "messages_per_month": 2000,
      "max_sessions": 3,
      "features": ["images", "scheduling", "priority_support"]
    },
    ...
  ]
}
```

---

#### POST `/billing/subscribe`

Create Stripe subscription for user.

**Body**:
```json
{
  "price_id": "price_pro_12345",
  "success_url": "https://app.yourdomain.com/billing?success=1",
  "cancel_url": "https://app.yourdomain.com/billing?cancel=1"
}
```

**Response** (200):
```json
{
  "checkout_url": "https://checkout.stripe.com/c/..."
}
```

Redirect user to this URL to complete payment.

---

#### GET `/billing/invoices`

List user's invoices (from Stripe).

**Response** (200):
```json
{
  "invoices": [
    {
      "id": "in_123",
      "number": "0001",
      "amount_due": 1500,
      "currency": "usd",
      "status": "paid",
      "hosted_invoice_url": "https://pay.stripe.com/...",
      "created_at": "2025-01-01T00:00:00Z"
    }
  ]
}
```

---

#### POST `/billing/cancel`

Cancel subscription (immediately or at period end).

**Body**:
```json
{
  "at_period_end": true
}
```

**Response** (200): `{ "subscription_status": "canceled" }`

---

### Webhooks (Stripe & OpenWA)

#### POST `/webhooks/stripe`

Stripe sends events (payment succeeded, failed, subscription updated).

**Headers**: `Stripe-Signature: t=...,v1=...`

**Response** (200): `{ "status": "received" }`

No authentication required (verify signature server-side).

Included events:
- `invoice.payment_succeeded`
- `invoice.payment_failed`
- `customer.subscription.updated`
- `customer.subscription.deleted`

---

#### POST `/webhooks/openwa`

OpenWA sends status updates (session connected, banned, message status).

**Headers**: Optional secret (HMAC verification recommended)

**Body** (example):
```json
{
  "event": "session.connected",
  "session_id": "user_1_session_a1b2",
  "phone_number": "+852...",
  "timestamp": "2025-01-15T10:30:00Z"
}
```

or

```json
{
  "event": "message.status",
  "message_id": "abc123",
  "status": "delivered",
  "timestamp": "2025-01-15T10:30:05Z"
}
```

**Response** (200): `{ "status": "ok" }`

**Processing**:
- Update `wa_sessions` status by matching `session_id` to our stored `session_id`
- Update `campaign_messages` by matching `message_id` to `whatsapp_message_id`
- If `status=failed` with reason `banned`, mark session as `banned` and notify user

---

## Error Responses

Standard error format:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "details": {
      "email": ["The email has already been taken."]
    }
  }
}
```

**HTTP Status Codes**:
- `200` - OK
- `201` - Created
- `202` - Accepted (async job started)
- `204` - No Content (deleted)
- `400` - Bad Request (invalid parameters)
- `401` - Unauthorized (invalid/missing token)
- `403` - Forbidden (quota exceeded, session not owned)
- `404` - Not Found
- `409` - Conflict (duplicate resource)
- `422` - Validation Failed
- `429` - Too Many Requests (rate limit)
- `500` - Internal Server Error
- `503` - Service Unavailable (OpenWA down)

---

## Rate Limits

Per user token:
- 60 requests/minute (configurable)
- Burst: 120

Per IP (unauthenticated):
- 5 requests/minute

Endpoints like `/campaigns/{id}/send` count as 1 request but trigger background jobs; the job itself sends messages at 1-2 sec intervals (rate-limited by OpenWA per session).

---

## Pagination

List endpoints use cursor-based pagination via `?page=1&per_page=50`. Meta block includes total count.

---

## File Uploads

Only media for campaigns:
- Upload to `/campaigns/{id}/media` (or provide URL in body)
- Supported: JPG, PNG, GIF, PDF (max 5MB)
- Return: `{ "url": "https://cdn.yourdomain.com/media/xyz.jpg" }`
- OpenWA needs publicly accessible URL; our CDN should serve it without auth

---

## Idempotency

Optional `Idempotency-Key` header on POST/PUT/DELETE to prevent duplicate operations. Store key + response for 24h.

---

## Webhooks Verification

- Stripe: verify signature using `stripe/webhook secret`
- OpenWA: shared secret in header `X-OpenWA-Signature` (HMAC-SHA256 of payload)

---

Next: See `docs/openwa-integration.md` for how OpenWA is managed.