---
title: "Step 1 of upload: get presigned upload URLs"
method: POST
path: "/events/{id}/photos"
tags: ["Photos"]
---

# Step 1 of upload: get presigned upload URLs

`POST /events/{id}/photos`

## Request

- Base URL: `https://findme.photo/api/v1`
- URL: `https://findme.photo/api/v1/events/{id}/photos`
- Auth: HTTP bearer

Reserves one upload slot per file (up to 50 per call) and returns a presigned S3 URL for each. The full upload flow:

1. **This call.** Send `filename`, `content_type` and `size_bytes` for each file. Accepted types: JPEG, PNG, WebP photos (up to 50 MB each) and MP4, QuickTime, WebM videos (up to 1 GB each); any other type or size rejects the whole batch with `400 invalid_request` and `details.rejected`. The plan's storage limit (and video storage limit, on plans that have one) is checked against the declared sizes; going over returns `429 quota_exceeded` with the byte counts in `details`.
2. **PUT the bytes.** For each slot, `PUT` the raw file bytes to `presigned_url` with the same `Content-Type` you declared, before `expires_at` (15 minutes). This request goes straight to S3: do not send your FindMe Authorization header. Resize photos to the album quality first: the long edge must not exceed 800, 3000 or 4000 px for those qualities, or 8000 px for `15mb` (the response repeats the album's `album_quality`).
3. **Confirm** the uploaded slots with `confirmPhotoUploads`. Until then the slot stays `pending` and the photo is not in the album.
4. **Release** any slot you will not fill (cancelled export, failed PUT) with `releasePhotoUploads`.

`batch_id` is informational; no other endpoint takes it. Not idempotent: every call reserves new slots.

## Request body

- CreatePhotoUploadsRequest
  - `files` object[], required
    - `filename` string, required
    - `content_type` 'image/jpeg' | 'image/png' | 'image/webp' | 'video/mp4' | 'video/quicktime' | 'video/webm', required: MIME type. Use the same value as the Content-Type of the S3 PUT.
    - `size_bytes` integer, required: File size in bytes. Max 52428800 (50 MB) for photos, 1073741824 (1 GB) for videos.

## Response `200`

Upload slots with presigned URLs.

- object
  - `data` PhotoUploadBatch, required
    - `batch_id` string, required: Informational only.
    - `uploads` object[], required: One slot per requested file, in request order.
      - `photo_id` string, uuid, required: Pass to `confirmPhotoUploads` after the PUT, or to `releasePhotoUploads` if unused.
      - `filename` string, required
      - `media_type` 'photo' | 'video', required
      - `presigned_url` string, uri, required: S3 URL to PUT the raw bytes to, with the declared Content-Type and no Authorization header.
      - `s3_key` string, required
      - `expires_at` string, date-time, required: The presigned URL stops working 15 minutes after issue.
    - `album_quality` '800px' | '3000px' | '4000px' | '15mb', required: Stored photo resolution for the album, by long edge: 800px, 3000px, 4000px, or `15mb` (up to 8000 px). Fixed once the album has photos.
  - `meta` Meta, required
    - `request_id` string, required

## Other responses

- `400`: `invalid_request`: invalid id, query parameter, cursor or body. Validation failures list `details.issues`.
- `401`: `missing_or_invalid_token` (no or malformed `Authorization: Bearer fm_live_...` header) or `invalid_token` (unknown or revoked key).
- `403`: `tier_not_eligible`: the plan does not include API access (includes `upgrade_url`), or does not include the feature this endpoint needs.
- `404`: `not_found`: no such resource, or it is not yours, or (for albums) it is deleted.
- `429`: `rate_limited` or `quota_exceeded` from the request gate (see TooManyRequests), or `quota_exceeded` because the batch would exceed the plan's storage limit (`details.storage_used_bytes`, `details.storage_limit_bytes`, `details.batch_bytes`) or video storage limit (`details.video_storage_used_bytes`, `details.video_storage_limit_bytes`, `details.batch_video_bytes`). The storage case carries no rate-limit headers.
- `500`: `internal_error`: something failed on our side. Safe to retry reads; include `request_id` when reporting.

---

[API](https://skmtc.dev/findme/apis/findme-photo-api.md) · [All operations](https://skmtc.dev/findme/apis/findme-photo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/findme/apis/findme-photo-api/revisions/b68670b742a9?raw)
