Swift examples
Snippets target Swift 5.7+ on iOS 15 / macOS 12 or newer, where URLSession has native async/await. The same code runs on Linux with the swift-corelibs-foundation URLSession or via AsyncHTTPClient (Swift on Server).
import Foundation
enum PhotoPick { static let api = URL(string: "https://api.photopick.cz/api/v1")! static let token = ProcessInfo.processInfo.environment["PHOTOPICK_API_KEY"]! // For iOS apps load the token from Keychain instead of env vars.}
extension URLRequest { static func photoPick(_ path: String, method: String = "GET") -> URLRequest { var req = URLRequest(url: PhotoPick.api.appendingPathComponent(path)) req.httpMethod = method req.setValue("Bearer \(PhotoPick.token)", forHTTPHeaderField: "Authorization") req.setValue("application/json", forHTTPHeaderField: "Accept") return req }}1. Verify your key (/me)
Section titled “1. Verify your key (/me)”struct RateLimit: Decodable { let limit: Int let remaining: Int let reset: Int}
struct Me: Decodable { let apiKeyId: Int let customerId: Int let name: String let scopes: [String] let rateLimit: RateLimit}
func whoami() async throws -> Me { let (data, response) = try await URLSession.shared.data(for: .photoPick("me")) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(Me.self, from: data)}
// Usagelet me = try await whoami()print(me.scopes, me.rateLimit.remaining)Required scope: customer:read.
2. List photos (paginated)
Section titled “2. List photos (paginated)”AsyncSequence that yields every photo across pages — constant memory, lazy.
struct Photo: Decodable { let id: Int let filename: String // ... add the fields you need}
struct Pagination: Decodable { let nextCursor: String? let hasMore: Bool}
struct PhotoPage: Decodable { let data: [Photo] let pagination: Pagination}
struct PhotoStream: AsyncSequence { typealias Element = Photo let pageSize: Int
struct Iterator: AsyncIteratorProtocol { let pageSize: Int var buffer: [Photo] = [] var cursor: String? = nil var done = false
mutating func next() async throws -> Photo? { if buffer.isEmpty { if done { return nil } var components = URLComponents(url: PhotoPick.api.appendingPathComponent("photos"), resolvingAgainstBaseURL: false)! components.queryItems = [URLQueryItem(name: "limit", value: String(pageSize))] + (cursor.map { [URLQueryItem(name: "cursor", value: $0)] } ?? [])
var req = URLRequest(url: components.url!) req.setValue("Bearer \(PhotoPick.token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: req) let page = try JSONDecoder().decode(PhotoPage.self, from: data) buffer = page.data cursor = page.pagination.nextCursor done = !page.pagination.hasMore } return buffer.isEmpty ? nil : buffer.removeFirst() } }
func makeAsyncIterator() -> Iterator { Iterator(pageSize: pageSize) }}
// Usagefor try await photo in PhotoStream(pageSize: 100) { print(photo.id, photo.filename)}Required scope: photos:read.
3. Fetch one photo + download original
Section titled “3. Fetch one photo + download original”URLSession.download streams to disk without holding the full file in memory.
func downloadPhoto(id: Int, to destination: URL) async throws -> Photo { // Metadata let (metaData, _) = try await URLSession.shared.data(for: .photoPick("photos/\(id)")) let meta = try JSONDecoder().decode(Photo.self, from: metaData)
// Bytes — follows redirects to the signed URL automatically let (tempURL, response) = try await URLSession.shared.download(for: .photoPick("photos/\(id)/download")) guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { throw URLError(.badServerResponse) } try? FileManager.default.removeItem(at: destination) try FileManager.default.moveItem(at: tempURL, to: destination) return meta}
let meta = try await downloadPhoto( id: 1234, to: URL(fileURLWithPath: "photo-1234.jpg"))print("Saved \(meta.filename)")Required scope: photos:read.
4. Tag CRUD
Section titled “4. Tag CRUD”struct Tag: Codable { let id: Int? let name: String let color: String?}
struct TagAssignment: Codable { let tagIds: [Int]}
func send<T: Decodable>(_ req: URLRequest, body: Encodable? = nil) async throws -> T { var req = req req.setValue("application/json", forHTTPHeaderField: "Content-Type") if let body { req.httpBody = try JSONEncoder().encode(body) }
let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(T.self, from: data)}
// Createlet created: Tag = try await send( .photoPick("tags", method: "POST"), body: Tag(id: nil, name: "Selects", color: "#e94b60"))
// Attach (full replace)let _: TagAssignment = try await send( .photoPick("photos/1234/tags", method: "PUT"), body: TagAssignment(tagIds: [created.id!]))
// Renamelet renamed: Tag = try await send( .photoPick("tags/\(created.id!)", method: "PATCH"), body: Tag(id: nil, name: "Final selects", color: nil))
// Deletelet (_, delResponse) = try await URLSession.shared.data( for: .photoPick("tags/\(created.id!)", method: "DELETE"))print((delResponse as! HTTPURLResponse).statusCode) // 204Required scopes: tags:write (create/rename/delete), photos:write (attach).
Retry pattern (rate limits + 5xx)
Section titled “Retry pattern (rate limits + 5xx)”func withRetry<T>(maxAttempts: Int = 5, _ block: () async throws -> (data: Data, response: URLResponse)) async throws -> Data { var lastResponse: URLResponse? for attempt in 1...maxAttempts { let (data, response) = try await block() lastResponse = response let http = response as! HTTPURLResponse
if (200..<300).contains(http.statusCode) { return data } if http.statusCode < 500 && http.statusCode != 429 { throw URLError(.badServerResponse) // 4xx — don't retry }
let retryAfter = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "") ?? attempt let backoff = min(retryAfter, Int(pow(2.0, Double(attempt)))) try await Task.sleep(nanoseconds: UInt64(backoff) * 1_000_000_000) } throw URLError(.timedOut)}
let data = try await withRetry { try await URLSession.shared.data(for: .photoPick("photos"))}See Rate limits and Errors for the full vocabulary.