# Listing & pagination

Listing walks the keys in a bucket. This page covers both clients, the delimiter and common-prefix model, and full
paging loops.

- The **native client** has `listObjects` with prefix/delimiter semantics and an iterator that follows continuation
  tokens.
- The **S3 client** has two forms: `ListObjectsV2` (the token-paged default) and `ListObjects` (the older marker-paged
  v1), plus paginators that follow the tokens for you.

For the operation matrix, see the [native data-plane reference](/reference/native-api) and the
[S3 operations reference](/reference/s3-operations#listing).

## One page

A single listing call returns one page. `prefix` filters by key prefix; `maxKeys` caps the page (the server caps it at
1000); the page reports `isTruncated` and a `nextContinuationToken` when more remains.

:::: native

::: code-group

```ts [Node]
const page = await native.listObjects("reports", { prefix: "q1/", maxKeys: 100 });
for (const o of page.objects) console.log(o.key, o.size);
console.log(page.isTruncated, page.nextContinuationToken);
```

```go [Go]
page, err := native.ListObjects(ctx, lockwellnative.ListObjectsInput{
    Bucket: "reports", Prefix: "q1/", MaxKeys: 100,
})
for _, o := range page.Objects {
    fmt.Println(o.Key, o.Size, o.ETag)
}
fmt.Println(page.IsTruncated, page.NextContinuationToken)
```

```java [Java]
// ListObjectsOptions record: prefix, delimiter, continuationToken, maxKeys.
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;

var page = nativeClient.listObjects("reports", new ListObjectsOptions("q1/", null, null, 100));
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
System.out.println(page.isTruncated() + " " + page.nextContinuationToken());
```

:::

::::

:::: s3

The S3 `ListObjectsV2` returns the same page shape:

::: code-group

```ts [Node]
const page = await s3.listObjectsV2("reports", { prefix: "q1/", maxKeys: 100 });
for (const o of page.objects) console.log(o.key, o.size, o.etag);
console.log(page.isTruncated, page.nextContinuationToken);
```

```go [Go]
page, err := s3.ListObjectsV2(ctx, "reports",
    lockwellsdk.WithPrefix("q1/"), lockwellsdk.WithMaxKeys(100))
for _, o := range page.Objects {
    fmt.Println(o.Key, o.Size, o.ETag)
}
fmt.Println(page.IsTruncated, page.NextContinuationToken)
```

```java [Java]
// listObjectsV2(bucket, prefix, maxKeys, continuationToken)
var page = s3.listObjectsV2("reports", "q1/", 100, null);
page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
System.out.println(page.truncated() + " " + page.nextContinuationToken());
```

:::

::::

### Listing options

| Option             | Native (Go / Node / Java)                                   | S3 (Go / Node / Java)                                 |
| ------------------ | ----------------------------------------------------------- | ----------------------------------------------------- |
| Key prefix         | `Prefix` / `prefix` / `ListObjectsOptions.prefix`           | `WithPrefix` / `prefix` / arg 2                       |
| Delimiter          | `Delimiter` / `delimiter` / `ListObjectsOptions.delimiter`  | `WithDelimiter` / `delimiter` / v1 form               |
| Start after a key  | `StartAfter` (Go) / `startAfter` / `startAfter` on the wire | `WithStartAfter` / `startAfter`                       |
| Continuation token | `ContinuationToken` / `continuationToken` / arg 3           | `WithContinuationToken` / `continuationToken` / arg 4 |
| Max keys per page  | `MaxKeys` / `maxKeys` / `ListObjectsOptions.maxKeys`        | `WithMaxKeys` / `maxKeys` / arg 3                     |

`startAfter` begins the listing after a given key (a one-shot starting point, not a resume token). `continuationToken`
resumes a previously truncated listing.

::: warning Pass `startAfter` or `continuationToken`, not both. They serve different purposes: one sets a starting key,
the other resumes a paged listing. :::

## Delimiters and common prefixes

A `delimiter` (almost always `"/"`) makes the listing behave like a directory walk. Keys that share a prefix up to the
next delimiter are collapsed into a single entry in `commonPrefixes`; only keys with no further delimiter past the
prefix appear in `objects`.

Given these keys:

```text
logs/2026/01/a.log
logs/2026/01/b.log
logs/2026/02/c.log
logs/index.txt
```

a listing with `prefix: "logs/"` and `delimiter: "/"` returns:

- `objects`: `logs/index.txt`
- `commonPrefixes`: `logs/2026/`

To descend, list again with the common prefix as the new `prefix`. Without a delimiter, the listing is flat: every key
under the prefix comes back in `objects`.

:::: native

::: code-group

```ts [Node]
const page = await native.listObjects("archive", { prefix: "logs/", delimiter: "/" });
console.log(page.commonPrefixes); // ["logs/2026/"]
for (const o of page.objects) console.log(o.key); // "logs/index.txt"
```

```go [Go]
page, _ := native.ListObjects(ctx, lockwellnative.ListObjectsInput{
    Bucket: "archive", Prefix: "logs/", Delimiter: "/",
})
fmt.Println(page.CommonPrefixes) // ["logs/2026/"]
for _, o := range page.Objects {
    fmt.Println(o.Key) // "logs/index.txt"
}
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;

// Native carries a delimiter directly:
var page = nativeClient.listObjects("archive", new ListObjectsOptions("logs/", "/", null, null));
System.out.println(page.commonPrefixes());
page.objects().forEach(o -> System.out.println(o.key()));
```

:::

::::

:::: s3

::: code-group

```ts [Node]
const page = await s3.listObjectsV2("archive", { prefix: "logs/", delimiter: "/" });
console.log(page.commonPrefixes); // ["logs/2026/"]
for (const o of page.objects) console.log(o.key); // "logs/index.txt"
```

```go [Go]
page, _ := s3.ListObjectsV2(ctx, "archive",
    lockwellsdk.WithPrefix("logs/"), lockwellsdk.WithDelimiter("/"))
fmt.Println(page.CommonPrefixes) // ["logs/2026/"]
for _, o := range page.Objects {
    fmt.Println(o.Key) // "logs/index.txt"
}
```

```java [Java]
// The S3 v1 listObjects carries the delimiter as its last argument:
var page = s3.listObjects("archive", "logs/", null, 1000, "/");
System.out.println(page.commonPrefixes());
page.objects().forEach(o -> System.out.println(o.key()));
```

:::

::::

## Paging across all keys

To walk every key, follow `isTruncated` with the continuation token until it clears. The native client ships an iterator
that does this for you; the S3 client ships paginators per listing operation.

The native iterator (`ListObjectsAll` in Go, the `paginateObjects` async generator in Node) transparently follows
continuation tokens.

:::: native

::: code-group

```ts [Node]
for await (const page of native.paginateObjects("reports", { prefix: "q1/" })) {
  for (const o of page.objects) console.log(o.key, o.size);
}
```

```go [Go]
it := native.ListObjectsAll(ctx, lockwellnative.ListObjectsInput{Bucket: "reports", Prefix: "q1/"})
for it.Next() {
    o := it.Object()
    fmt.Println(o.Key, o.Size)
}
if err := it.Err(); err != nil {
    return err
}
```

```java [Java]
// Page the native listing with the continuation token:
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;

String token = null;
do {
    var page = nativeClient.listObjects("reports", new ListObjectsOptions("q1/", null, token, null));
    page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
    token = page.isTruncated() ? page.nextContinuationToken() : null;
} while (token != null);
```

:::

::::

:::: s3

The S3 client ships an auto-pager for each listing operation, so you never thread a continuation token by hand. The loop
shape is the same in every language: ask whether more pages remain, fetch the next page, repeat.

::: code-group

```ts [Node]
// Node uses async iterators rather than a paginator object:
for await (const page of s3.paginateObjectsV2("reports", { prefix: "q1/" })) {
  for (const o of page.objects) console.log(o.key, o.size);
}
```

```go [Go]
p := s3.NewListObjectsV2Paginator("reports", lockwellsdk.WithPrefix("q1/"))
for p.HasMorePages() {
    page, err := p.NextPage(ctx)
    if err != nil {
        return err
    }
    for _, o := range page.Objects {
        fmt.Println(o.Key, o.Size)
    }
}
```

```java [Java]
// The S3 client exposes paginators for versions, multipart uploads, and parts.
// For a flat object listing, page with the continuation token:
String token = null;
do {
    var page = s3.listObjectsV2("reports", "q1/", 1000, token);
    page.objects().forEach(o -> System.out.println(o.key() + " " + o.size()));
    token = page.truncated() ? page.nextContinuationToken() : null;
} while (token != null);
```

:::

The Go paginators (`NewListObjectsV2Paginator`, `NewListObjectVersionsPaginator`, `NewListMultipartUploadsPaginator`,
`NewListPartsPaginator`) own the tokens. Seed them with the same options the one-shot method takes, but do not pass a
continuation/marker yourself: the paginator manages it.

::::

## Paging by hand

If you would rather drive the loop yourself, follow `isTruncated` with the continuation token until it clears.

:::: native

::: code-group

```ts [Node]
let token;
do {
  const page = await native.listObjects(
    "reports",
    token ? { prefix: "q1/", continuationToken: token } : { prefix: "q1/" },
  );
  for (const o of page.objects) console.log(o.key);
  token = page.isTruncated ? page.nextContinuationToken : undefined;
} while (token);
```

```go [Go]
var token string
for {
    in := lockwellnative.ListObjectsInput{Bucket: "reports", Prefix: "q1/", ContinuationToken: token}
    page, err := native.ListObjects(ctx, in)
    if err != nil {
        return err
    }
    for _, o := range page.Objects {
        fmt.Println(o.Key)
    }
    if !page.IsTruncated || page.NextContinuationToken == "" {
        break
    }
    token = page.NextContinuationToken
}
```

```java [Java]
import com.lockwell.sdk.nativeapi.NativeTypes.ListObjectsOptions;

String token = null;
do {
    var page = nativeClient.listObjects("reports", new ListObjectsOptions("q1/", null, token, null));
    page.objects().forEach(o -> System.out.println(o.key()));
    token = page.isTruncated() ? page.nextContinuationToken() : null;
} while (token != null);
```

:::

::::

:::: s3

::: code-group

```ts [Node]
let token;
do {
  const page = await s3.listObjectsV2(
    "reports",
    token ? { prefix: "q1/", continuationToken: token } : { prefix: "q1/" },
  );
  for (const o of page.objects) console.log(o.key);
  token = page.isTruncated ? page.nextContinuationToken : undefined;
} while (token);
```

```go [Go]
var token string
for {
    opts := []lockwellsdk.ListOption{lockwellsdk.WithPrefix("q1/")}
    if token != "" {
        opts = append(opts, lockwellsdk.WithContinuationToken(token))
    }
    page, err := s3.ListObjectsV2(ctx, "reports", opts...)
    if err != nil {
        return err
    }
    for _, o := range page.Objects {
        fmt.Println(o.Key)
    }
    if !page.IsTruncated || page.NextContinuationToken == "" {
        break
    }
    token = page.NextContinuationToken
}
```

```java [Java]
String token = null;
do {
    var page = s3.listObjectsV2("reports", "q1/", 1000, token);
    page.objects().forEach(o -> System.out.println(o.key()));
    token = page.truncated() ? page.nextContinuationToken() : null;
} while (token != null);
```

:::

::::

## ListObjects v1 (marker pagination)

The S3 client also exposes the v1 `ListObjects`, which pages by a `marker` (the last key returned) rather than an opaque
token. Prefer `ListObjectsV2` (or the native listing) for new code; v1 exists for parity with clients that still page by
marker.

:::: s3

::: code-group

```ts [Node]
let marker;
do {
  const page = await s3.listObjects("reports", marker ? { prefix: "v1/", marker } : { prefix: "v1/" });
  for (const o of page.objects) console.log(o.key);
  marker = page.isTruncated ? page.nextMarker || page.objects.at(-1)?.key : undefined;
} while (marker);
```

```go [Go]
page1, _ := s3.ListObjects(ctx, "reports",
    lockwellsdk.WithPrefix("v1/"), lockwellsdk.WithMaxKeys(2))
page2, _ := s3.ListObjects(ctx, "reports",
    lockwellsdk.WithPrefix("v1/"), lockwellsdk.WithMaxKeys(2),
    lockwellsdk.WithMarker(page1.NextMarker))
```

```java [Java]
// listObjects(bucket, prefix, marker, maxKeys, delimiter)
var page1 = s3.listObjects("reports", "v1/", null, 2, null);
var page2 = s3.listObjects("reports", "v1/", page1.nextMarker(), 2, null);
```

:::

::::

When a delimiter collapses the truncation boundary onto a common prefix, the server may omit `NextMarker`. In that case
resume from the last key or common prefix you saw, which is what the Node and Go paginators do for you.

## Listing versions and multipart uploads

Versioned listings (`ListObjectVersions`) and in-progress multipart uploads (`ListMultipartUploads`, `ListParts`) share
this prefix/delimiter/marker model. They each have a dedicated page:

- [Versioning](/guide/versioning): list versions and delete markers.
- [Multipart uploads](/guide/multipart-uploads): list uploads and their parts.

## Next steps

- [Upload & download](/guide/data-operations): the put/get/head you are listing.
- [Copying objects](/guide/copying-objects): duplicate keys you found in a listing.
- [Deleting objects](/guide/deleting-objects): batch-delete a listed prefix.
