# R2 API Reference

## PUT (Upload)

```typescript
// Basic
await env.MY_BUCKET.put(key, value)

// With metadata
await env.MY_BUCKET.put(key, value, {
  httpMetadata: {
    contentType: 'image/jpeg',
    contentDisposition: 'attachment; filename="photo.jpg"',
    cacheControl: 'max-age=3600',
  },
  customMetadata: { userId: '123', version: '2' },
  storageClass: 'Standard', // or 'InfrequentAccess'
  sha256: arrayBufferOrHex, // Integrity check
  ssecKey: arrayBuffer32bytes, // SSE-C encryption
})

// Value types: ReadableStream | ArrayBuffer | string | Blob
```

## GET (Download)

```typescript
const object = await env.MY_BUCKET.get(key)
if (!object) return new Response('Not found', { status: 404 })

// Body: arrayBuffer(), text(), json(), blob(), body (ReadableStream)

// Ranged reads
const object = await env.MY_BUCKET.get(key, { range: { offset: 0, length: 1024 } })

// Conditional GET
const object = await env.MY_BUCKET.get(key, { onlyIf: { etagMatches: '"abc123"' } })
```

## HEAD (Metadata Only)

```typescript
const object = await env.MY_BUCKET.head(key) // Returns R2Object without body
```

## DELETE

```typescript
await env.MY_BUCKET.delete(key)
await env.MY_BUCKET.delete([key1, key2, key3]) // Batch (max 1000)
```

## LIST

```typescript
const listed = await env.MY_BUCKET.list({
  limit: 1000,
  prefix: 'photos/',
  cursor: cursorFromPrevious,
  delimiter: '/',
  include: ['httpMetadata', 'customMetadata'],
})

// Pagination (always use truncated flag)
while (listed.truncated) {
  const next = await env.MY_BUCKET.list({ cursor: listed.cursor })
  listed.objects.push(...next.objects)
  listed.truncated = next.truncated
  listed.cursor = next.cursor
}
```

## Multipart Uploads

```typescript
const multipart = await env.MY_BUCKET.createMultipartUpload(key, {
  httpMetadata: { contentType: 'video/mp4' },
})

const uploadedParts: R2UploadedPart[] = []
for (let i = 0; i < partCount; i++) {
  const part = await multipart.uploadPart(i + 1, partData)
  uploadedParts.push(part)
}

const object = await multipart.complete(uploadedParts)
// OR: await multipart.abort();

// Resume
const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId)
```

## Presigned URLs (S3 SDK)

```typescript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

const s3 = new S3Client({
  region: 'auto',
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
  credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY },
})

const uploadUrl = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), {
  expiresIn: 3600,
})
return Response.json({ uploadUrl })
```

## TypeScript Interfaces

```typescript
interface R2Bucket {
  head(key: string): Promise<R2Object | null>
  get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>
  put(
    key: string,
    value: ReadableStream | ArrayBuffer | string | Blob,
    options?: R2PutOptions
  ): Promise<R2Object | null>
  delete(keys: string | string[]): Promise<void>
  list(options?: R2ListOptions): Promise<R2Objects>
  createMultipartUpload(key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload>
  resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload
}

interface R2Object {
  key: string
  version: string
  size: number
  etag: string
  httpEtag: string // httpEtag is quoted, use for headers
  uploaded: Date
  httpMetadata?: R2HTTPMetadata
  customMetadata?: Record<string, string>
  storageClass: 'Standard' | 'InfrequentAccess'
  checksums: R2Checksums
  writeHttpMetadata(headers: Headers): void
}

interface R2ObjectBody extends R2Object {
  body: ReadableStream
  bodyUsed: boolean
  arrayBuffer(): Promise<ArrayBuffer>
  text(): Promise<string>
  json<T>(): Promise<T>
  blob(): Promise<Blob>
}

interface R2HTTPMetadata {
  contentType?: string
  contentDisposition?: string
  contentEncoding?: string
  contentLanguage?: string
  cacheControl?: string
  cacheExpiry?: Date
}

interface R2PutOptions {
  httpMetadata?: R2HTTPMetadata | Headers
  customMetadata?: Record<string, string>
  sha256?: ArrayBuffer | string // Only ONE checksum allowed
  storageClass?: 'Standard' | 'InfrequentAccess'
  ssecKey?: ArrayBuffer
}

interface R2GetOptions {
  onlyIf?: R2Conditional | Headers
  range?: R2Range | Headers
  ssecKey?: ArrayBuffer
}

interface R2ListOptions {
  limit?: number
  prefix?: string
  cursor?: string
  delimiter?: string
  startAfter?: string
  include?: ('httpMetadata' | 'customMetadata')[]
}

interface R2Objects {
  objects: R2Object[]
  truncated: boolean
  cursor?: string
  delimitedPrefixes: string[]
}

interface R2Conditional {
  etagMatches?: string
  etagDoesNotMatch?: string
  uploadedBefore?: Date
  uploadedAfter?: Date
}

interface R2Range {
  offset?: number
  length?: number
  suffix?: number
}

interface R2Checksums {
  md5?: ArrayBuffer
  sha1?: ArrayBuffer
  sha256?: ArrayBuffer
  sha384?: ArrayBuffer
  sha512?: ArrayBuffer
}

interface R2MultipartUpload {
  key: string
  uploadId: string
  uploadPart(
    partNumber: number,
    value: ReadableStream | ArrayBuffer | string | Blob
  ): Promise<R2UploadedPart>
  abort(): Promise<void>
  complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>
}

interface R2UploadedPart {
  partNumber: number
  etag: string
}
```

## CLI Operations

```bash
wrangler r2 object put my-bucket/file.txt --file=./local.txt
wrangler r2 object get my-bucket/file.txt --file=./download.txt
wrangler r2 object delete my-bucket/file.txt
wrangler r2 object list my-bucket --prefix=photos/
```
