# Pushr — Local Setup & REST API Reference

Setup state captured: **15 May 2026**. Validated against this WAMP install.

---

## 1. Local environment (no Docker)

| Component | Value |
|---|---|
| Web root | `c:\wamp64\www\product` |
| App URL | <http://localhost/product/> |
| PHP | served by `wampapache64` (running) |
| MySQL | `wampmysqld64` 8.4.7 — `127.0.0.1:3306`, user `root`, no password |
| Database | `pusher66` (24 tables, seeded from [install/dump.sql](install/dump.sql)) |
| Cache | `files` (see [config.php](config.php)) |
| Installer marker | [install/installed](install/installed) — already present |

If you ever need to reinstall from scratch:

```powershell
# 1. Drop & recreate DB, reimport dump
$mysql = "C:\wamp64\bin\mysql\mysql8.4.7\bin\mysql.exe"
& $mysql -uroot -e "DROP DATABASE IF EXISTS pusher66; CREATE DATABASE pusher66 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
& $mysql -uroot pusher66 < "c:\wamp64\www\product\install\dump.sql"

# 2. Make sure the installer flag exists so the installer doesn't run again
New-Item -ItemType File -Force -Path "c:\wamp64\www\product\install\installed"
```

`config.php` is already pointing at `pusher66` / `root` / empty password and `SITE_URL = http://localhost/product/`. No changes required.

### Cron (only if you use scheduled features)

The platform ships a CLI cron at [app/controllers/Cron.php](app/controllers/Cron.php). On Windows, register it with Task Scheduler to run every minute:

```powershell
$php  = "C:\wamp64\bin\php\php8.3.14\php.exe"   # adjust to your installed PHP
$work = "c:\wamp64\www\product"
schtasks /Create /SC MINUTE /MO 1 /TN "Pushr Cron" `
  /TR "`"$php`" `"$work\index.php`" cron" /F
```

---

## 2. The API key

Per-user API keys are stored in `users.api_key` (32 hex chars). The admin user already has one.

### Get the current key

```powershell
& "C:\wamp64\bin\mysql\mysql8.4.7\bin\mysql.exe" -uroot pusher66 `
  -e "SELECT user_id, email, api_key FROM users;"
```

Captured during setup:

| user_id | email | type | api_key |
|---|---|---|---|
| 1 | `admin@local.test` | 1 (admin) | `4b63cb99298e277fdb74d4ec5b8d16d4` |

### Rotate the key

Either visit <http://localhost/product/account-api> (handled by [app/controllers/AccountApi.php](app/controllers/AccountApi.php)) and click *Generate*, **or** rotate via SQL:

```sql
UPDATE users
SET api_key = LOWER(HEX(RANDOM_BYTES(16)))
WHERE user_id = 1;
```

Then flush the per-user cache so the new key is recognised on the next request:

```powershell
Remove-Item -Recurse -Force "c:\wamp64\www\product\uploads\cache\*"
```

### Required flags for the API to work

Both must be true (already configured here):

1. Global: `settings.value->main.api_is_enabled = true` — toggled from the admin panel.
2. Per-user/plan: `users.plan_settings->api_is_enabled = true`.

If you ever get `401 no_access` on a valid key, one of these is off.

---

## 3. Authentication

All requests use a Bearer token:

```
Authorization: Bearer 4b63cb99298e277fdb74d4ec5b8d16d4
Content-Type: application/json
Accept: application/json
```

Auth handler: [app/traits/Apiable.php](app/traits/Apiable.php). It also enforces:

- **Rate limit**: 60 requests / 60 s per key (non-admin). Returns `429` with `X-RateLimit-*` headers.
- **Demo guard**: disabled in this build.

Errors follow the JSON:API shape:

```json
{ "errors": [{ "title": "Unauthorized.", "status": "401" }] }
```

Successes:

```json
{ "data": { ... }, "meta": { ... }, "links": { ... } }
```

---

## 4. Endpoints

Base URL: `http://localhost/product/api`
Admin base URL: `http://localhost/product/admin-api`  *(requires `users.type = 1`)*

Sources of truth: route table in [app/core/Router.php](app/core/Router.php#L1107) and the controllers in [app/controllers/api/](app/controllers/api/) and [app/controllers/admin-api/](app/controllers/admin-api/).

Each resource controller dispatches by HTTP method:

- `GET /resource` → list (paginated, supports `?page=`, `?search=`, `?order_by=`, `?order_type=`, `?results_per_page=`, plus per-resource filters)
- `GET /resource/{id}` → fetch one
- `POST /resource` → create
- `POST /resource/{id}` → update (server treats `POST + id` as PATCH)
- `DELETE /resource/{id}` → delete

### User-scoped endpoints

| Resource | Path | Verbs | Controller |
|---|---|---|---|
| Current user | `/api/user` | GET | [ApiUser.php](app/controllers/api/ApiUser.php) |
| Websites | `/api/websites` | GET, POST, DELETE | [ApiWebsites.php](app/controllers/api/ApiWebsites.php) |
| Subscribers | `/api/subscribers` | GET, POST, DELETE | [ApiSubscribers.php](app/controllers/api/ApiSubscribers.php) |
| Subscribers stats | `/api/subscribers-statistics` | GET | [ApiSubscribersStatistics.php](app/controllers/api/ApiSubscribersStatistics.php) |
| Subscribers logs | `/api/subscribers-logs` | GET | [ApiSubscribersLogs.php](app/controllers/api/ApiSubscribersLogs.php) |
| Campaigns | `/api/campaigns` | GET, POST, DELETE | [ApiCampaigns.php](app/controllers/api/ApiCampaigns.php) |
| Recurring campaigns | `/api/recurring-campaigns` | GET, POST, DELETE | [ApiRecurringCampaigns.php](app/controllers/api/ApiRecurringCampaigns.php) |
| Personal notifications | `/api/personal-notifications` | GET, POST, DELETE | [ApiPersonalNotifications.php](app/controllers/api/ApiPersonalNotifications.php) |
| RSS automations | `/api/rss-automations` | GET, POST, DELETE | [ApiRssAutomations.php](app/controllers/api/ApiRssAutomations.php) |
| Flows | `/api/flows` | GET, POST, DELETE | [ApiFlows.php](app/controllers/api/ApiFlows.php) |
| Segments | `/api/segments` | GET, POST, DELETE | [ApiSegments.php](app/controllers/api/ApiSegments.php) |
| Domains | `/api/domains` | GET, POST, DELETE | [ApiDomains.php](app/controllers/api/ApiDomains.php) |
| Notification handlers | `/api/notification-handlers` | GET, POST, DELETE | [ApiNotificationHandlers.php](app/controllers/api/ApiNotificationHandlers.php) |
| Teams | `/api/teams` | GET, POST, DELETE | [ApiTeams.php](app/controllers/api/ApiTeams.php) |
| Teams (member view) | `/api/teams-member` | GET | [ApiTeamsMember.php](app/controllers/api/ApiTeamsMember.php) |
| Team members | `/api/team-members` | GET, POST, DELETE | [ApiTeamMembers.php](app/controllers/api/ApiTeamMembers.php) |
| Payments | `/api/payments` | GET | [ApiPayments.php](app/controllers/api/ApiPayments.php) |
| Audit logs | `/api/logs` | GET | [ApiLogs.php](app/controllers/api/ApiLogs.php) |

### Admin-only endpoints (`users.type = 1`)

| Resource | Path | Controller |
|---|---|---|
| Users | `/admin-api/users` | [AdminApiUsers.php](app/controllers/admin-api/AdminApiUsers.php) |
| Plans | `/admin-api/plans` | [AdminApiPlans.php](app/controllers/admin-api/AdminApiPlans.php) |
| Payments | `/admin-api/payments` | [AdminApiPayments.php](app/controllers/admin-api/AdminApiPayments.php) |
| Domains | `/admin-api/domains` | [AdminApiDomains.php](app/controllers/admin-api/AdminApiDomains.php) |
| SSO | `/admin-api/sso` | [AdminApiSSO.php](app/controllers/admin-api/AdminApiSSO.php) |
| Dynamic OG images | `/admin-api/dynamic-og-images` | [AdminApiDynamicOgImages.php](app/controllers/admin-api/AdminApiDynamicOgImages.php) |

---

## 5. Quick examples

### `GET /api/user`

```powershell
$key = "4b63cb99298e277fdb74d4ec5b8d16d4"
Invoke-RestMethod -Uri "http://localhost/product/api/user" `
  -Headers @{ Authorization = "Bearer $key" }
```

Verified response (truncated):

```json
{
  "data": {
    "id": 1,
    "name": "AltumCode",
    "email": "admin@local.test",
    "plan_settings": { "api_is_enabled": true, "websites_limit": -1, ... }
  }
}
```

### `GET /api/websites?page=1&results_per_page=10`

```bash
curl -H "Authorization: Bearer 4b63cb99298e277fdb74d4ec5b8d16d4" \
     "http://localhost/product/api/websites?page=1&results_per_page=10"
```

### `POST /api/campaigns` — create

```bash
curl -X POST "http://localhost/product/api/campaigns" \
  -H "Authorization: Bearer 4b63cb99298e277fdb74d4ec5b8d16d4" \
  -H "Content-Type: application/json" \
  -d '{
    "website_id": 1,
    "name": "Launch blast",
    "title": "We are live!",
    "body": "Come check it out.",
    "url": "https://example.com"
  }'
```

> Required fields per resource live in each controller's `patch()` method — the source is the spec.

### `POST /api/campaigns/123` — update

```bash
curl -X POST "http://localhost/product/api/campaigns/123" \
  -H "Authorization: Bearer 4b63cb99298e277fdb74d4ec5b8d16d4" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Renamed" }'
```

### `DELETE /api/campaigns/123`

```bash
curl -X DELETE "http://localhost/product/api/campaigns/123" \
  -H "Authorization: Bearer 4b63cb99298e277fdb74d4ec5b8d16d4"
```

---

## 6. Smoke test

A ready-to-run PowerShell script lives at [tools/api-smoke-test.ps1](tools/api-smoke-test.ps1):

```powershell
cd c:\wamp64\www\product
powershell -ExecutionPolicy Bypass -File tools\api-smoke-test.ps1
```

It hits every read-only endpoint with the configured key and prints status codes.

---

## 7. Troubleshooting

| Symptom | Likely cause |
|---|---|
| `401 no_bearer` | Missing/garbled `Authorization` header. Double-check the `Bearer ` prefix. |
| `401 no_access` | Wrong key, user disabled (`status != 1`), `settings.main.api_is_enabled = false`, or `plan_settings.api_is_enabled = false`. |
| `404` on every API URL | `.htaccess` not active — confirm Apache `mod_rewrite` is on (it is in default WAMP). Or the URL is under a different folder than `/product/`. |
| `429 rate_limit` | More than 60 req/min on this key. Wait or use the admin key (admins are not rate-limited). |
| Stale data after key rotation | Clear `uploads/cache/`. |
