# Call logs

The Core API stores manually logged call attempts, including unanswered calls. Each call belongs to one dataset and links to at least one candidate, contact, client, job, or application.

## Endpoints

| Method | Path | OAuth scope |
| --- | --- | --- |
| POST | `/core/call-logs` | `callLogs:write` |
| GET | `/core/call-logs` | `callLogs:read` |
| GET | `/core/call-logs/{id}` | `callLogs:read` |
| PATCH | `/core/call-logs/{id}` | `callLogs:write` |
| DELETE | `/core/call-logs/{id}` | `callLogs:write` |

`callLogs:*` also covers these operations. Add the required scopes to the OAuth client before obtaining a token. Existing tokens do not automatically gain new scopes.

## Create

```json
{
  "occurred_at": "2026-09-21T09:30:00+08:00",
  "direction": "outbound",
  "outcome": "connected",
  "duration_seconds": 240,
  "user_dataset_id": "11111111-1111-4111-8111-111111111111",
  "from_phone": "+60123456789",
  "to_phone": "+441234567890",
  "summary": "Discussed availability. Candidate can interview next week.",
  "transcript": {
    "bucket": "agency-call-transcripts",
    "key": "calls/2026/09/21/interview.txt"
  },
  "links": [
    {
      "record_type": "candidate",
      "record_id": "22222222-2222-4222-8222-222222222222"
    }
  ]
}
```

Required: `occurred_at`, `direction`, `outcome`, and `links`.

- `direction`: `inbound` or `outbound`.
- `outcome`: `connected`, `no_answer`, `left_voicemail`, `missed`, `busy`, or `failed`.
- `user_dataset_id`: optional, nullable `user_datasets.id` of the person who made or received the call. Omit it or use null when the user is unknown. A supplied ID must belong to the current dataset. PATCH with null clears the attribution; omission preserves it. Historical attribution remains after membership removal.
- `created_by`: filled from the authenticated actor, independently of `user_dataset_id`; clients cannot set it.
- `duration_seconds`: optional nonnegative integer; null means unknown and zero means zero seconds.
- `from_phone`, `to_phone`, `summary`, and `transcript`: optional and nullable. Phone numbers are stored as supplied after trimming, with a 100-character limit. Summary limit: 100,000 characters.
- `links`: 1–100 unique `{ record_type, record_id }` pairs. Supported types: `candidate`, `contact`, `client`, `job`, `application`. IDs must be local Quake IDs from this dataset. A call can link to multiple records without duplicating the call.
- `transcript`: an S3 bucket name and object key (at most 1,024 UTF-8 bytes). This is a reference only. The API does not verify object existence, upload, retrieve, sign, transcribe, or delete the object. Bucket names and keys are returned only to callers authorized to read the call.

POST returns `201` with the full call, including `id`, `dataset_id`, `created_by`, links, nullable fields, and timestamps.

## Timestamps

All timestamps accept ISO 8601 with a timezone and are returned in UTC.

| Field | Meaning | Writable |
| --- | --- | --- |
| `occurred_at` | When the call happened | Yes |
| `created_at` | Business/source creation time; defaults to server time | Yes |
| `updated_at` | Business/source update time; defaults to server time on create and update | Yes |
| `sys_created_at` | When Quake stored the call | No |
| `sys_updated_at` | Latest Quake write, including link-only changes | No |

Supplying either `sys_*` field is rejected. Preserving old business timestamps never backdates the system timestamps.

## Update and delete

PATCH is partial: omitted fields are preserved; null clears nullable fields. A supplied `links` array atomically replaces all links and must remain nonempty. Invalid references roll back the entire write. Empty updates and unknown fields are rejected. `id`, `dataset_id`, and `created_by` are immutable.

DELETE returns `204` and permanently removes the call and its links. It retains the S3 object referenced by `transcript`.

## List

Filters combine with AND:

- `record_type` and `record_id` together.
- `user_dataset_id`, `direction`, `outcome`.
- `occurred_from`, `occurred_to`: inclusive timestamps.
- `sys_updated_from`: inclusive server-update timestamp.
- `limit`: 1–100, default 50; `offset`: nonnegative, default 0.

Results are ordered by `occurred_at DESC, id DESC`. Example response:

```json
{ "data": [], "has_more": false, "limit": 50, "offset": 0 }
```

This is offset pagination, not a change stream; concurrent writes can move records between pages. Deleted calls are not returned as tombstones.

## Access

App tokens operate within their dataset according to `callLogs` scopes. Delegated sessions also require the exact `callLogs` action permission (`view`, `create`, `update`, or `delete`). They must have the corresponding read scope, view permission, and access-group visibility for **every** linked record. A call's summary or transcript can describe all its linked records, so partial visibility does not expose the call.

Visibility filtering happens before pagination. Hidden or cross-dataset call IDs return `404`; invalid or inaccessible submitted references return `422` (or `403` when access to the record type is missing). Missing scopes/permissions return `403`; malformed inputs return `400`.
