# PDFPrime Mobile API v1

Production base URL:

```text
https://pdf.techicomm.com/api/v1
```

All production requests must use HTTPS. JSON responses use UTF-8. The request ID is returned in the `X-Request-ID` response header and in error `data.request_id`. File upload uses `multipart/form-data`; every other POST uses `application/json`.

## Common response format

Every successful object response has exactly this top-level structure:

```json
{
  "status": true,
  "message": "",
  "data": {}
}
```

Every successful list response puts the list directly inside `data`:

```json
{
  "status": true,
  "message": "",
  "data": []
}
```

All errors use the same structure with `status: false`. Use `data.code` for app logic and `message` for display:

```json
{
  "status": false,
  "message": "Please check the supplied fields.",
  "data": {
    "code": "validation_failed",
    "request_id": "b347...",
    "fields": {
      "email": "Enter a valid email address."
    }
  }
}
```

## Authentication

The mobile API uses a short-lived access token and a rotating refresh token:

- Access token lifetime: 15 minutes.
- Refresh token lifetime: 60 days.
- Store both tokens only in Android Keystore / iOS Keychain using `flutter_secure_storage`.
- Send the access token as `Authorization: Bearer pdfm_at_...`.
- When an endpoint returns `401` with `token_expired`, call refresh once, save the new token pair, and retry the original request.
- Refresh tokens rotate. Delete the old pair immediately after a successful refresh.
- Tokens are hashed with SHA-256 in MySQL. Raw token values are returned once and are never stored by the server.

### Register

`POST /auth/register.php`

```json
{
  "name": "Lorick Kardwal",
  "email": "lorick@example.com",
  "password": "StrongPass1",
  "password_confirmation": "StrongPass1",
  "accepted_terms": true,
  "device_name": "Lorick iPhone"
}
```

### Login

`POST /auth/login.php`

```json
{
  "email": "lorick@example.com",
  "password": "StrongPass1",
  "device_name": "Lorick iPhone"
}
```

Successful register/login response:

```json
{
  "status": true,
  "message": "",
  "data": {
    "user": {
      "id": 42,
      "name": "Lorick Kardwal",
      "email": "lorick@example.com",
      "plan": "free",
      "role": "user",
      "avatar_url": ""
    },
    "auth": {
      "token_type": "Bearer",
      "access_token": "pdfm_at_...",
      "expires_in": 900,
      "refresh_token": "pdfm_rt_...",
      "refresh_expires_in": 5184000,
      "session_id": "..."
    }
  }
}
```

### Google/Firebase login

Authenticate in Flutter with Firebase Auth, obtain the Firebase ID token, then call `POST /auth/firebase.php`:

```json
{
  "id_token": "FIREBASE_ID_TOKEN",
  "device_name": "Pixel 9"
}
```

The response is the same user + auth object returned by normal login.

### Refresh

`POST /auth/refresh.php`

```json
{
  "refresh_token": "pdfm_rt_...",
  "device_name": "Lorick iPhone"
}
```

### Logout

`POST /auth/logout.php` with the access-token header:

```json
{"all_devices": false}
```

Set `all_devices` to `true` to revoke every mobile session belonging to the user.

## User endpoints

| Method | Endpoint | Purpose |
|---|---|---|
| GET | `/user/profile.php` | User profile and plan limits |
| GET | `/user/jobs.php?page=1&limit=20` | Paginated document history; list is returned directly in `data` |
| POST | `/user/push-token.php` | Register or refresh this device's FCM token |
| DELETE | `/user/push-token.php` | Remove one token, or all push devices for the current user |
| GET | `/tools/index.php` | Complete file/AI tool catalog and option schemas |
| GET | `/health.php` | API health check; no authentication required |

### Register for push notifications

After Firebase Messaging returns a registration token, send it to:

`POST /user/push-token.php`

```json
{
  "token": "FCM_REGISTRATION_TOKEN",
  "platform": "android",
  "device_id": "stable-app-install-id",
  "app_version": "1.0.0",
  "enabled": true
}
```

Register the token again whenever Firebase refreshes it. The response includes
`default_topic`; subscribe the device to that Firebase topic so Admin → Push
notifications can send scalable all-user campaigns.

Before logout, remove the current token:

```http
DELETE /user/push-token.php
```

```json
{"token":"FCM_REGISTRATION_TOKEN"}
```

Omit `token` to remove every push device belonging to the current user.

## File tool workflow

All PDF and image tools use the same secure three-step flow.

### 1. Upload

`POST /jobs/upload.php`

Headers:

```text
Authorization: Bearer ACCESS_TOKEN
Content-Type: multipart/form-data
```

Multipart fields:

- `tool`: tool key from `/tools/index.php`.
- `files[]`: one or more files. Use the tool catalog's `min_files`, `max_files`, and `extensions`.

Example:

```bash
curl -X POST "$BASE/jobs/upload.php" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F "tool=merge" \
  -F "files[]=@first.pdf" \
  -F "files[]=@second.pdf"
```

The response object in `data` contains `job.id`, `page_count`, usage, `status_url`, and `process_url`.

### 2. Process

`POST /jobs/process.php`

```json
{
  "job_id": "32_character_job_id",
  "options": {
    "output_name": "merged-document"
  }
}
```

This endpoint currently completes processing synchronously. Keep the Flutter request timeout at 180 seconds for large documents and AI background operations. Calling it again for a completed job is idempotent and returns the existing download URL.

Every tool's supported `options` are returned by `/tools/index.php`. Important examples:

```json
{"job_id":"...","options":{"compression_level":"recommended"}}
```

```json
{"job_id":"...","options":{"selected_pages":[1,3,5]}}
```

```json
{"job_id":"...","options":{"output_format":"webp","quality":90}}
```

```json
{"job_id":"...","options":{"ocr_language":"auto","ocr_mode":"document"}}
```

### 3. Status and download

- `GET /jobs/status.php?id=JOB_ID`
- `GET /jobs/download.php?id=JOB_ID`

The download endpoint also requires `Authorization: Bearer ACCESS_TOKEN`. Download URLs are not public and cannot be used by a different account.

Files expire according to the user's plan (normally 24 hours). Save the response directly to an application cache/document path.

## AI text tools

Get available tool keys from `/tools/index.php`, then call `POST /ai/generate.php`:

```json
{
  "tool": "article-writer",
  "input": "Write an article about secure online PDF tools.",
  "tone": "professional",
  "language": "English",
  "length": "medium"
}
```

Allowed tones: `professional`, `friendly`, `creative`, `persuasive`, `simple`.

Allowed languages: `English`, `Hindi`, `Hinglish`, `Spanish`, `French`, `German`, `Portuguese`, `Arabic`.

Allowed lengths: `short`, `medium`, `long`.

QR generation is intentionally device-side. Use `qr_flutter` so links/text never leave the phone.

## Flutter integration with Dio

Recommended packages:

```yaml
dependencies:
  dio: ^5.0.0
  flutter_secure_storage: ^9.0.0
  file_picker: ^8.0.0
  path_provider: ^2.0.0
  firebase_auth: ^5.0.0
  google_sign_in: ^6.0.0
  qr_flutter: ^4.0.0
```

Create a singleton API client. Never put tokens in logs, analytics, crash reports, URLs, or SharedPreferences.

```dart
class PdfPrimeApi {
  PdfPrimeApi(this.secureStorage)
      : dio = Dio(BaseOptions(
          baseUrl: 'https://pdf.techicomm.com/api/v1',
          connectTimeout: const Duration(seconds: 20),
          receiveTimeout: const Duration(seconds: 180),
        ));

  final Dio dio;
  final FlutterSecureStorage secureStorage;

  Future<void> initialize() async {
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        final token = await secureStorage.read(key: 'access_token');
        if (token != null) options.headers['Authorization'] = 'Bearer $token';
        options.headers['Accept'] = 'application/json';
        handler.next(options);
      },
      onError: (error, handler) async {
        final data = error.response?.data;
        final expired = error.response?.statusCode == 401 &&
            data is Map && data['data']?['code'] == 'token_expired';
        if (!expired || error.requestOptions.extra['retried'] == true) {
          return handler.next(error);
        }
        try {
          await refresh();
          final request = error.requestOptions;
          request.extra['retried'] = true;
          request.headers['Authorization'] =
              'Bearer ${await secureStorage.read(key: 'access_token')}';
          return handler.resolve(await dio.fetch(request));
        } catch (_) {
          await clearTokens();
          return handler.next(error);
        }
      },
    ));
  }

  Future<void> refresh() async {
    final refreshToken = await secureStorage.read(key: 'refresh_token');
    final response = await Dio(BaseOptions(baseUrl: dio.options.baseUrl)).post(
      '/auth/refresh.php',
      data: {'refresh_token': refreshToken, 'device_name': 'Flutter app'},
    );
    await saveAuth(response.data['data']['auth']);
  }

  Future<void> saveAuth(Map<String, dynamic> auth) async {
    await secureStorage.write(key: 'access_token', value: auth['access_token']);
    await secureStorage.write(key: 'refresh_token', value: auth['refresh_token']);
  }

  Future<void> clearTokens() async {
    await secureStorage.delete(key: 'access_token');
    await secureStorage.delete(key: 'refresh_token');
  }
}
```

Upload:

```dart
final form = FormData.fromMap({
  'tool': 'compress',
  'files[]': await MultipartFile.fromFile(path, filename: fileName),
});
final upload = await api.dio.post('/jobs/upload.php', data: form);
final jobId = upload.data['data']['job']['id'];
final processed = await api.dio.post('/jobs/process.php', data: {
  'job_id': jobId,
  'options': {'compression_level': 'recommended'},
});
```

Download:

```dart
await api.dio.download(
  '/jobs/download.php?id=$jobId',
  destinationPath,
  options: Options(responseType: ResponseType.bytes),
);
```

## Error contract

```json
{
  "status": false,
  "message": "Please check the supplied fields.",
  "data": {
    "code": "validation_failed",
    "request_id": "b347...",
    "fields": {"email": "Enter a valid email address."}
  }
}
```

Use `data.code` for application logic and top-level `message` for display. Send `data.request_id` or the `X-Request-ID` header to support when reporting a server problem.

Common HTTP statuses:

- `400`: invalid JSON/request.
- `401`: missing, expired, or invalid authentication.
- `403`: suspended account or forbidden action.
- `404`: job does not exist, belongs to another account, or expired.
- `409`: duplicate account or invalid job state.
- `415`: wrong content type.
- `422`: validation or processing error.
- `429`: rate/quota limit; observe `Retry-After`.
- `500/503`: server dependency unavailable.

## Flutter UI theme mapping

Use the website's visual language:

- Primary red: `#EF4038`
- Ink/navy: `#172033`
- Muted text: `#7B8497`
- Soft background: `#F7F8FB`
- Success green: `#23A66F`
- Cards: white, 16–20 px radius, subtle navy shadow
- Headings: Manrope 700/800
- Body: DM Sans 400/500/600

The mobile app should show the same three processing states as the website: Upload → Customize → Download.

## Production checklist

- Run `php scripts/setup-database.php` after deployment.
- Keep `PDFPRIME_API_SECRET` stable, private, and at least 32 characters.
- Set `APP_URL=https://pdf.techicomm.com`.
- Keep HTTPS enabled and HTTP redirected to HTTPS.
- Do not allow wildcard CORS. Native Flutter apps do not require CORS.
- Redact `Authorization`, passwords, Firebase ID tokens, and refresh tokens from logs.
- Use Android Network Security Config and iOS ATS defaults to block cleartext HTTP.
- Consider certificate pinning only if the team can safely ship certificate rotations.

Machine-readable contract: [`openapi.json`](openapi.json).
