# Agent API contract

Base path: `/api/agent`. Requests and responses use JSON, except result submissions, which use `multipart/form-data` so the evidence image or PDF can travel with the result.

## Sign in

`POST /api/agent/token` (maximum 6 attempts per minute)

```json
{"email":"agent@example.test","password":"…","device_name":"Android device"}
```

Only accounts with an active polling station assignment and the `agent` role in that campaign may sign in. The endpoint returns a Sanctum bearer token with `agent:read` and `agent:submit-results` abilities. Tokens expire after 30 days. Store the token securely on the device and send it as `Authorization: Bearer <token>`. `DELETE /api/agent/token` revokes the current token.

## Download assignments

`GET /api/agent/assignments`

Returns only active assignments belonging to the authenticated user and within the monitored candidate's election and geographic contest scope. Each item includes station details, the valid candidate list, and whether a result has already been submitted.

## Submit a result

`POST /api/agent/results` (bearer token with `agent:submit-results`)

Send multipart fields:

| Field | Value |
| --- | --- |
| `client_submission_id` | Client-generated UUID, retained unchanged for every retry of this submission |
| `assignment_id` | ID from the assignments response |
| `votes[n][candidate_id]`, `votes[n][votes]` | Repeat for each candidate, for example `votes[0][candidate_id]=12`, `votes[0][votes]=180` |
| `valid_votes`, `total_votes_cast`, `rejected_votes`, `spoilt_votes`, `disputed_votes`, `rejection_objected_votes` | Nonnegative integer counts |
| `evidence` | Required JPG, PNG, WebP, or PDF file up to 10 MB |

The server verifies the user's assignment, station and contest scope, candidate list, and vote totals; stores the evidence with its SHA-256 hash; and creates the result in `pending` review status. A successful first submission returns HTTP 201. Retrying the same UUID from the same agent returns the original result with `replayed: true`; use a new UUID only for a genuinely new submission. A station and contest can have one result.

## Sync behavior

Keep pending submissions on the device until a 201 or replayed 200 response arrives. Retry network failures using the same UUID and unchanged payload. A 409 means that a station/contest result already exists, or that the UUID belongs to another agent; refresh assignments and flag the submission for manual resolution rather than silently replacing it. A 422 response contains field validation errors. A 401 response means the token is missing, invalid, or expired; sign in again.

This API does not provide a Flutter client yet. The endpoint contract is ready for that later phase.
