# CopperChest API Contract

## Response Envelope

Success:

```json
{ "ok": true, "data": {} }
```

Error:

```json
{ "ok": false, "error": { "code": "bad_credentials", "message": "Email or password is incorrect." } }
```

## Bootstrap

### `POST /v1/start/checkpoint`

Plain local body:

```json
{
  "install_key": "apps-flyer-id",
  "os_name": "iOS 18.0",
  "app_bundle": "com.example.CopperChest",
  "firebase_project": "123456789",
  "store_ref": "1234567890",
  "push_ref": "fcm-token-if-known",
  "locale": "en_US",
  "agent": "Mozilla/5.0 ..."
}
```

Production body:

```json
{ "payload": "base64url({\"iv\":\"...\",\"tag\":\"...\",\"data\":\"...\"})" }
```

Response:

```json
{ "ok": true, "data": { "device_id": "uuid", "accepted": true } }
```

### `POST /v1/start/resolve`

Same fields as checkpoint, plus:

```json
{
  "idfa": "00000000-0000-0000-0000-000000000000",
  "conversion": {
    "af_status": "Non-organic",
    "media_source": "source",
    "campaign": "campaign"
  }
}
```

If app already has an access token, send `Authorization: Bearer <access_token>`. API verifies token in this final bootstrap request and links device to account.

Response:

```json
{
  "ok": true,
  "data": {
    "device_id": "uuid",
    "auth": { "authorized": true, "account": { "id": "uuid", "email": "user@example.com" } },
    "analytics_url": "https://example.com/offer",
    "message": null
  }
}
```

When external analytics returns `{"ok":true,"url":"..."}`, response includes:

```http
analytics-service: https://example.com/offer
```

## Auth

### `POST /v1/session/create`

```json
{ "email": "user@example.com", "password": "password-123", "name": "User" }
```

### `POST /v1/session/open`

```json
{ "email": "user@example.com", "password": "password-123" }
```

Auth response:

```json
{
  "ok": true,
  "data": {
    "account_id": "uuid",
    "access_token": "jwt",
    "refresh_token": "opaque-token",
    "expires_in": 900
  }
}
```

### `POST /v1/session/refresh`

```json
{ "refresh_token": "opaque-token" }
```

### `POST /v1/session/close`

```json
{ "refresh_token": "opaque-token" }
```

## Domain Payloads

All domain routes are authenticated.

Each entity row stores Swift model JSON under `payload`. Server adds sync metadata:

```json
{
  "id": "uuid",
  "payload": {},
  "version": 1,
  "created_at": "2026-10-05T00:00:00Z",
  "updated_at": "2026-10-05T00:00:00Z",
  "deleted_at": null,
  "deleted": false
}
```

Resource names:

- `wallets`
- `transactions`
- `envelopes`
- `count-sessions`
- `reminders`
- `allocation-events`
- `categories`
- `app-state`
- `drafts`
- `attachments`

Create:

```http
POST /v1/wallets
Authorization: Bearer <access_token>
```

```json
{
  "id": "client-generated-uuid",
  "payload": {
    "id": "client-generated-uuid",
    "name": "Cash",
    "currencyCode": "USD",
    "decimalPlaces": 2,
    "startingAmount": 10000,
    "isArchived": false,
    "createdAt": "2026-10-05T00:00:00Z",
    "note": "",
    "hasRecordedTransaction": false
  }
}
```

Update:

```json
{
  "version": 1,
  "payload": { "name": "Cash Updated" }
}
```

Delete:

```json
{ "version": 2 }
```

## Sync

### Pull

```json
{ "cursor": "2026-10-05T00:00:00Z" }
```

Response:

```json
{
  "ok": true,
  "data": {
    "cursor": "2026-10-05T00:01:00Z",
    "resources": {
      "wallets": [],
      "transactions": []
    }
  }
}
```

### Push

```json
{
  "changes": [
    {
      "resource": "transactions",
      "id": "uuid",
      "version": 1,
      "deleted": false,
      "payload": {
        "walletId": "uuid",
        "type": "expense",
        "amount": 2500,
        "date": "2026-10-05T00:00:00Z",
        "category": "Food"
      }
    }
  ]
}
```

### Exchange

`POST /v1/sync/exchange` combines push then pull.

## External Forward

Server forwards final attribution to `ANALYTICS_FORWARD_URL`.

Body uses fixed analytics keys:

```json
{
  "af_id": "apps-flyer-id",
  "os": "iOS 18.0",
  "bundle_id": "com.example.CopperChest",
  "firebase_project_id": "123456789",
  "store_id": "1234567890",
  "push_token": "fcm-token",
  "locale": "en_US",
  "idfa": "idfa",
  "source_ip": "203.0.113.10",
  "af_status": "Non-organic",
  "media_source": "source",
  "campaign": "campaign"
}
```

`User-Agent` is sent as an HTTP header, not in body.
