---
title: "client.Forms"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/csharp/reference/forms"
area: "C#"
category: "Reference"
---

# client.Forms

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Forms that add the people who fill them in to your audiences, with an optional confirmation email first: make them from a starter or your own fields, publish, pause and copy them, read their analytics, manage what people sent, and sign someone up without a credential.

### `Forms.ListAsync`

List one page of the sign-up forms in the workspace

```csharp
Task<Page> ListAsync(
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns one page of the sign-up forms the caller can reach, newest first, without their documents or settings. Paging is keyset: `limit:` takes 1 to 100 and defaults to 25, and `nextCursor` goes back as `cursor:` while `hasMore` is true. `ListAllAsync` and `IterateAsync` do that walk for you.

Each form carries its `status`, `url`, the hosted page that shows it, `subscribeUrl`, where a plain HTML form posts, `audienceIds`, `doubleOptIn`, `hasUnpublishedChanges` and `stats`. `stats` is counted at the moment of the read: `views` of the hosted page and the embed while the form was live, `submissions` stored, `added`, the sign-ups added to the audiences, `pending`, the ones still to confirm, and `lastSubmittedAt`.

Read one with `GetAsync` for the draft `document`, the `publishedDocument` visitors see and the `settings`.

Scopes: `forms:read`.

**Parameters**

- `limit` (`int?`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`string?`): The `nextCursor` from the previous page, passed back exactly as it came. One the server cannot read is a 400 `invalid_cursor`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `Page` with `items`, `hasMore` and `nextCursor`. Each item has `id`, `name`, `description`, `status`, `url`, `subscribeUrl`, `audienceIds`, `doubleOptIn`, `hasUnpublishedChanges`, `stats`, `publishedAt`, `createdAt` and `updatedAt`.

**Example**

```csharp
var page = await client.Forms.ListAsync(limit: 50);

foreach (var form in page)
{
    Console.WriteLine($"{form["name"]} {form["status"]}, {form["stats"]?["submissions"]} sign-ups");
}
```

**Notes**

- An app a member connected lists only the forms that member made, as the console does for them. An API key and an app the owner connected list every form in the workspace.
- Read only, so the SDK retries it after a network failure like any other read.

Also available in: API [`GET /forms`](https://openemail.uk/docs/api/reference/forms#get-forms); TypeScript [`forms.list()`](https://openemail.uk/docs/sdk/reference/forms#list); Python [`forms.list()`](https://openemail.uk/docs/python/reference/forms#list); Ruby [`forms.list`](https://openemail.uk/docs/ruby/reference/forms#list); PHP [`forms->list`](https://openemail.uk/docs/php/reference/forms#list); Go [`Forms.List`](https://openemail.uk/docs/go/reference/forms#list); Java [`forms().list`](https://openemail.uk/docs/java/reference/forms#list); CLI [`openemail forms list`](https://openemail.uk/docs/cli/reference/forms#forms-list).

### `Forms.ListAllAsync`

Collect every sign-up form you can reach into one object

```csharp
Task<IReadOnlyList<JsonObject>> ListAllAsync(
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Follows `nextCursor` from page to page and returns every form the caller can reach, newest first, in the shape `ListAsync` returns. `limit:` sets the page size of each request, not the total.

Scopes: `forms:read`.

**Parameters**

- `limit` (`int?`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`string?`): A `nextCursor` from an earlier page to start after.
- `apiKey` (`string?`): Overrides the client's API key for every page of this walk.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A list of `JsonObject` items holding every form across all pages, each shaped like an item of `ListAsync`.

**Example**

```csharp
var forms = await client.Forms.ListAllAsync(limit: 100);

Console.WriteLine(forms.Count);
```

**Notes**

- A failure on any page throws out of the whole call.

Also available in: API [`GET /forms`](https://openemail.uk/docs/api/reference/forms#get-forms); TypeScript [`forms.listAll()`](https://openemail.uk/docs/sdk/reference/forms#listAll); Python [`forms.list_all()`](https://openemail.uk/docs/python/reference/forms#listAll); Ruby [`forms.list_all`](https://openemail.uk/docs/ruby/reference/forms#listAll); PHP [`forms->listAll`](https://openemail.uk/docs/php/reference/forms#listAll); Go [`Forms.ListAll`](https://openemail.uk/docs/go/reference/forms#listAll); Java [`forms().listAll`](https://openemail.uk/docs/java/reference/forms#listAll).

### `Forms.IterateAsync`

Stream the sign-up forms you can reach one at a time

```csharp
IAsyncEnumerable<JsonObject> IterateAsync(
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns an `IAsyncEnumerable<JsonObject>` that yields forms one by one, newest first, and requests the next page only once the current one is drained. Breaking out of the loop stops the requests.

Scopes: `forms:read`.

**Parameters**

- `limit` (`int?`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`string?`): A `nextCursor` from an earlier page to start after.
- `apiKey` (`string?`): Overrides the client's API key for every page of this walk.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

An `IAsyncEnumerable<JsonObject>` that yields one form per step.

**Example**

```csharp
await foreach (var form in client.Forms.IterateAsync())
{
    if ((bool?)form["hasUnpublishedChanges"] == true)
    {
        Console.WriteLine($"Unpublished edits: {form["name"]}");
    }
}
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in: API [`GET /forms`](https://openemail.uk/docs/api/reference/forms#get-forms); TypeScript [`forms.iterate()`](https://openemail.uk/docs/sdk/reference/forms#iterate); Python [`forms.iterate()`](https://openemail.uk/docs/python/reference/forms#iterate); Ruby [`forms.iterate`](https://openemail.uk/docs/ruby/reference/forms#iterate); PHP [`forms->iterate`](https://openemail.uk/docs/php/reference/forms#iterate); Go [`Forms.Iterate`](https://openemail.uk/docs/go/reference/forms#iterate); Java [`forms().iterate`](https://openemail.uk/docs/java/reference/forms#iterate).

### `Forms.CreateAsync`

Create a sign-up form

```csharp
Task<JsonObject> CreateAsync(
    IReadOnlyDictionary<string, object?> body,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Makes a draft form and returns it whole, in the shape `GetAsync` returns. Its fields, copy and style come from `document` when you send one, from the starter named in `starter` when you do not, and otherwise from a one-field email form. `settings` changes any of the defaults, which add people to the default audience only, send no confirmation email, show the thank-you message from the copy and notify nobody.

A form takes no sign-ups until it is published. Send `["publish"] = true` to put it live in the same call, or call `publish` later. Nothing is created when the document or settings break a rule.

Once it is live, share it with `url`, point a plain HTML `<form method="post">` at `subscribeUrl`, embed it with the script at `/embed/form.js` on the OpenEmail web app, or sign people up from your own code with `SubscribeAsync`.

Scopes: `forms:write`.

**Parameters**

- `name` (`string`, required): What the workspace calls the form, trimmed, 1 to 120 characters. Visitors never see it, and it need not be unique.
- `description` (`string`): A note for the workspace, at most 500 characters. Visitors never see it.
- `starter` (`string`): Begin from a starter: `blank`, `newsletter`, `waitlist`, `event`, `early-access` or `contact`, as in `OpenEmail\Constants\FormStarterSlugs`. Ignored when `document` is sent.
- `document` (`dictionary`): The whole form, `fields`, `copy` and `style`, in the shape `GetAsync` and `GetStarterAsync` return. Every field carries all 15 of its keys, with null for the ones it does not use, and a document needs exactly one email field, keyed `email` and required, and its field keys and ids must be unique. Leave it out to use `starter` or a one-field email form.
- `settings.audienceIds` (`IEnumerable<string>`): The audiences every sign-up joins, at most 20 ids such as `aud_9f2c4b7e1a0d63d84c5f2e7b`. Every contact is in the default audience as well, so an empty list adds people there only. An id that is not an audience the caller can reach is a 404 `audience_not_found`.
- `settings.doubleOptIn` (`bool`): Email each person a confirmation link and add them only once they open it. Defaults to false. True needs `senderAddress`.
- `settings.senderAddress` (`string`): The workspace address confirmation emails come from, required for `doubleOptIn`. It has to be an address the caller may send as, or the call is refused with 422 `form_sender_refused`.
- `settings.confirmSubject` (`string`): The subject of the confirmation email, 1 to 200 characters. Defaults to Please confirm your subscription.
- `settings.confirmMessage` (`string`): The text of the confirmation email above its button, at most 2000 characters.
- `settings.confirmButton` (`string`): The label of the confirmation button, 1 to 60 characters. Defaults to Confirm my subscription.
- `settings.successAction` (`string`): What a person sees after signing up: `message`, the thank-you copy in the document, which is the default, or `redirect`, which sends them to `redirectUrl`. `OpenEmail\Constants\FormSuccessActions` names them.
- `settings.redirectUrl` (`string`): The http or https page `redirect` sends people to, at most 2000 characters. Required while `successAction` is `redirect`.
- `settings.notifyAddresses` (`IEnumerable<string>`): Up to 10 addresses of this workspace that get an email about each sign-up. Each has to be one the caller can read, or the call is refused with 422 `invalid_form`.
- `publish` (`bool`): Publish in the same call, so the form takes sign-ups at once.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form as `GetAsync` returns it: the fields of a `ListAsync` item, with `status` `draft`, or `live` when `publish` was true, plus `document`, `publishedDocument`, `settings`, `audiences`, `senderIssue` and `senderProblem`.

**Example**

```csharp
using OpenEmail.Constants;

var form = await client.Forms.CreateAsync(new Body
{
    ["name"] = "Newsletter",
    ["starter"] = FormStarterSlugs.Newsletter,
    ["settings"] = new Body { ["audienceIds"] = new[] { "aud_9f2c4b7e1a0d63d84c5f2e7b" } },
    ["publish"] = true,
});

Console.WriteLine($"{form["id"]} is {form["status"]} at {form["url"]}");
```

**Notes**

- A value of the wrong type or length is 422 `invalid_parameter` with `param` naming it, and a key the body does not take is 422 `unknown_parameter`. A document or settings that break a rule, such as a form without its required email field, `doubleOptIn` without `senderAddress` or `redirect` without `redirectUrl`, is 422 `invalid_form`.
- A key or app limited to particular addresses may only name addresses it holds in `senderAddress` and `notifyAddresses`. Anything else is 422 `capability_unsupported`.
- Turning on `doubleOptIn`, naming a `senderAddress` or changing the confirmation email also needs `emails:send`, because the form then sends mail for you.
- A workspace holds 100 forms by default, and support can raise that for a workspace that needs more. The next one past the limit is 422 `workspace_limit_reached`.
- Not retried automatically, and nothing deduplicates by name, so a retry after a lost response can leave two forms. List them and delete the spare.

Also available in: API [`POST /forms`](https://openemail.uk/docs/api/reference/forms#post-forms); TypeScript [`forms.create()`](https://openemail.uk/docs/sdk/reference/forms#create); Python [`forms.create()`](https://openemail.uk/docs/python/reference/forms#create); Ruby [`forms.create`](https://openemail.uk/docs/ruby/reference/forms#create); PHP [`forms->create`](https://openemail.uk/docs/php/reference/forms#create); Go [`Forms.Create`](https://openemail.uk/docs/go/reference/forms#create); Java [`forms().create`](https://openemail.uk/docs/java/reference/forms#create); CLI [`openemail forms create`](https://openemail.uk/docs/cli/reference/forms#forms-create).

### `Forms.DesignAsync`

Design a form from a brief

```csharp
Task<JsonObject> DesignAsync(
    IReadOnlyDictionary<string, object?> body,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

A designer builds a new sign-up form from a written brief, as Create with AI does on the Forms page of the app: it picks the fields, writes the copy and sets the look, then saves the form as a draft. Review it with `GetAsync`, change it with `UpdateAsync` or `RedesignAsync`, and put it live with `PublishAsync`.

Put everything the form must ask and say in `brief`: what it is for, the fields, the wording word for word, the colours and the tone. It spends one AI action and can take up to half a minute.

Scopes: `forms:write`.

**Parameters**

- `brief` (`string`, required): Everything the form must ask and say, up to 4,000 characters.
- `name` (`string`): A short name for the form, up to 120 characters. Left out, the designer names it.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the new draft as `GetAsync` returns it, plus `design`, whose `notes` say what the designer adjusted or left out.

**Example**

```csharp
var form = await client.Forms.DesignAsync(new Body
{
    ["brief"] = "A waitlist for our beta: email, first name and company, with a short line on what joining means. Dark green button that says Join the waitlist.",
});

Console.WriteLine($"{form["id"]} {form["status"]}");
Console.WriteLine($"{string.Join(Environment.NewLine, form["design"]?["notes"]?.AsArray() ?? [])}");
```

**Notes**

- The SDK does not retry it, because a second call designs and saves a second form.
- A design that cannot be made valid is a 422 `invalid_form`, a workspace that already holds as many forms as it may a 422 `workspace_limit_reached`, a server with no model a 409 `ai_not_configured`, and a workspace out of AI actions a 429 `ai_quota_exceeded`. Nothing is created in any of them.

Also available in: API [`POST /forms/design`](https://openemail.uk/docs/api/reference/forms#post-forms-design); TypeScript [`forms.design()`](https://openemail.uk/docs/sdk/reference/forms#design); Python [`forms.design()`](https://openemail.uk/docs/python/reference/forms#design); Ruby [`forms.design`](https://openemail.uk/docs/ruby/reference/forms#design); PHP [`forms->design`](https://openemail.uk/docs/php/reference/forms#design); Go [`Forms.Design`](https://openemail.uk/docs/go/reference/forms#design); Java [`forms().design`](https://openemail.uk/docs/java/reference/forms#design); CLI [`openemail forms design`](https://openemail.uk/docs/cli/reference/forms#forms-design).

### `Forms.RedesignAsync`

Change a form’s design from instructions

```csharp
Task<JsonObject> RedesignAsync(
    string id,
    IReadOnlyDictionary<string, object?> body,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

A designer applies written instructions to the draft of a form and leaves the rest alone, as Ask AI does in the form builder of the app: add, remove or reorder fields, make one required, rewrite or translate the copy, change colours or fonts. The change is saved to the draft, so a live form keeps showing its published version until `PublishAsync`.

Send the `updatedAt` you read as `expectedUpdatedAt` to refuse the change when somebody saved the form since. It spends one AI action and can take up to half a minute.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `instructions` (`string`, required): What to change, with every detail, up to 4,000 characters.
- `expectedUpdatedAt` (`string`): The `updatedAt` you read, as it came. When the form was saved since, nothing is written and the answer is 409 `version_conflict`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form with the change in its draft, plus `design`, whose `notes` say what the designer changed.

**Example**

```csharp
var form = await client.Forms.GetAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

var changed = await client.Forms.RedesignAsync(form["id"]!.GetValue<string>(), new Body
{
    ["instructions"] = "Add a required company field after the name, and make the button say Join.",
    ["expectedUpdatedAt"] = form["updatedAt"],
});

Console.WriteLine($"{string.Join(Environment.NewLine, changed["design"]?["notes"]?.AsArray() ?? [])}");
```

**Notes**

- The SDK does not retry it, because each call is a fresh design pass.
- The same errors as `DesignAsync`, plus 404 `form_not_found` for a form the caller cannot reach and 409 `version_conflict` when the form was saved since `expectedUpdatedAt`, or while the designer worked.

Also available in: API [`POST /forms/{id}/redesign`](https://openemail.uk/docs/api/reference/forms#post-forms-id-redesign); TypeScript [`forms.redesign()`](https://openemail.uk/docs/sdk/reference/forms#redesign); Python [`forms.redesign()`](https://openemail.uk/docs/python/reference/forms#redesign); Ruby [`forms.redesign`](https://openemail.uk/docs/ruby/reference/forms#redesign); PHP [`forms->redesign`](https://openemail.uk/docs/php/reference/forms#redesign); Go [`Forms.Redesign`](https://openemail.uk/docs/go/reference/forms#redesign); Java [`forms().redesign`](https://openemail.uk/docs/java/reference/forms#redesign); CLI [`openemail forms redesign`](https://openemail.uk/docs/cli/reference/forms#forms-redesign).

### `Forms.ListStartersAsync`

List the starting points for a new form

```csharp
Task<IReadOnlyList<JsonObject>> ListStartersAsync(
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns the starters the console offers when somebody makes a form, as a plain list: `blank`, just the email field, then `newsletter`, `waitlist`, `event`, `early-access` and `contact`. Each carries its `slug`, `name` and `description`, and the documents are left out.

Pass a `slug` to `CreateAsync` as `starter` to begin a form from it, or read it whole with `GetStarterAsync` to change its fields first.

Starters are part of the product rather than workspace data, so the answer is the same for every key and changes only when a release adds one.

Scopes: `forms:read`.

**Parameters**

- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A list of `JsonObject` items, each with `object` set to `form_starter`, `slug`, `name` and `description`.

**Example**

```csharp
foreach (var starter in (await client.Forms.ListStartersAsync()))
{
    Console.WriteLine($"{starter["slug"]}: {starter["name"]}");
}
```

**Notes**

- Not paginated, and there is no cursor. The list is short.

Also available in: API [`GET /forms/starters`](https://openemail.uk/docs/api/reference/forms#get-forms-starters); TypeScript [`forms.listStarters()`](https://openemail.uk/docs/sdk/reference/forms#listStarters); Python [`forms.list_starters()`](https://openemail.uk/docs/python/reference/forms#listStarters); Ruby [`forms.list_starters`](https://openemail.uk/docs/ruby/reference/forms#listStarters); PHP [`forms->listStarters`](https://openemail.uk/docs/php/reference/forms#listStarters); Go [`Forms.ListStarters`](https://openemail.uk/docs/go/reference/forms#listStarters); Java [`forms().listStarters`](https://openemail.uk/docs/java/reference/forms#listStarters); CLI [`openemail forms list-starters`](https://openemail.uk/docs/cli/reference/forms#forms-list-starters).

### `Forms.GetStarterAsync`

Retrieve one starter with its document

```csharp
Task<JsonObject> GetStarterAsync(
    string slug,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns one starter in full: everything `ListStartersAsync` carries plus `document`, the fields, copy and style a form made from it begins with.

The document is there so a client can change it before it creates a form, rather than creating from the starter and updating afterwards. Send it, changed or not, as `document` on `CreateAsync`.

Scopes: `forms:read`.

**Parameters**

- `slug` (`string`, required): A starter slug from `listStarters`: `blank`, `newsletter`, `waitlist`, `event`, `early-access` or `contact`, as in `OpenEmail\Constants\FormStarterSlugs`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object`, `slug`, `name` and `description`, plus `document` with `fields`, `copy` and `style`.

**Example**

```csharp
using OpenEmail.Constants;

var starter = await client.Forms.GetStarterAsync(FormStarterSlugs.Newsletter);

var document = starter["document"];

var form = await client.Forms.CreateAsync(new Body { ["name"] = "Product news", ["document"] = document });

Console.WriteLine($"{form["id"]}");
```

**Notes**

- An unknown slug is a 404 `form_starter_not_found`.
- Passing `["starter"] = "newsletter"` to `CreateAsync` does the same seeding on the server, in one call instead of two.

Also available in: API [`GET /forms/starters/{slug}`](https://openemail.uk/docs/api/reference/forms#get-forms-starters-slug); TypeScript [`forms.getStarter()`](https://openemail.uk/docs/sdk/reference/forms#getStarter); Python [`forms.get_starter()`](https://openemail.uk/docs/python/reference/forms#getStarter); Ruby [`forms.get_starter`](https://openemail.uk/docs/ruby/reference/forms#getStarter); PHP [`forms->getStarter`](https://openemail.uk/docs/php/reference/forms#getStarter); Go [`Forms.GetStarter`](https://openemail.uk/docs/go/reference/forms#getStarter); Java [`forms().getStarter`](https://openemail.uk/docs/java/reference/forms#getStarter); CLI [`openemail forms get-starter`](https://openemail.uk/docs/cli/reference/forms#forms-get-starter).

### `Forms.GetAsync`

Retrieve a form with its documents and settings

```csharp
Task<JsonObject> GetAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns the whole form: the fields `ListAsync` carries with fresh `stats`, the draft `document`, the `publishedDocument` visitors see, which is null until the first publish, the `settings` and the named `audiences` sign-ups join.

`hasUnpublishedChanges` is true while the draft differs from what the live form shows. Settings take effect when they are saved, published or not.

For a form with `doubleOptIn` on, `senderIssue` says why confirmation emails cannot go out right now: `missing` when no `senderAddress` is set, `not_sendable` when the address cannot send, and `not_allowed` when its owner may not send as it. `senderProblem` says the same in a sentence. Both are null when confirmations can go out.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `id`, `name`, `description`, `status`, `url`, `subscribeUrl`, `audienceIds`, `doubleOptIn`, `hasUnpublishedChanges`, `stats`, `publishedAt`, `createdAt` and `updatedAt`, plus `document`, `publishedDocument`, `settings`, `senderIssue`, `senderProblem` and `audiences`, each an object with `id`, `name` and `builtin`.

**Example**

```csharp
var form = await client.Forms.GetAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

Console.WriteLine($"{form["status"]} at {form["url"]}");
Console.WriteLine($"Fields: {string.Join(", ", (form["document"]?["fields"]?.AsArray() ?? []).Select(row => row?["key"]))}");

if (form["senderIssue"] is not null)
{
    Console.WriteLine($"{form["senderProblem"]}");
}
```

**Notes**

- A form in another workspace is a 404 `form_not_found`, never a 403. An app a member connected reaches only the forms that member made.
- `audienceIds` and `settings.audienceIds` leave out an audience that was deleted after it was chosen, or that the caller cannot reach.

Also available in: API [`GET /forms/{id}`](https://openemail.uk/docs/api/reference/forms#get-forms-id); TypeScript [`forms.get()`](https://openemail.uk/docs/sdk/reference/forms#get); Python [`forms.get()`](https://openemail.uk/docs/python/reference/forms#get); Ruby [`forms.get`](https://openemail.uk/docs/ruby/reference/forms#get); PHP [`forms->get`](https://openemail.uk/docs/php/reference/forms#get); Go [`Forms.Get`](https://openemail.uk/docs/go/reference/forms#get); Java [`forms().get`](https://openemail.uk/docs/java/reference/forms#get); CLI [`openemail forms get`](https://openemail.uk/docs/cli/reference/forms#forms-get).

### `Forms.UpdateAsync`

Change a form's name, document or settings

```csharp
Task<JsonObject> UpdateAsync(
    string id,
    IReadOnlyDictionary<string, object?> patch,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

A partial update: a field you leave out keeps its stored value, and `["description"] = null` clears the note. `document` replaces the draft whole, and a live form keeps showing its published copy until `PublishAsync`. `settings` is merged field by field, so `new Body { ["settings"] = new Body { ["doubleOptIn"] = true, ["senderAddress"] = "hello@acme.com" } }` changes those two and keeps the rest, and settings take effect at once, live form or not.

To avoid overwriting somebody else's change, read the form and send its `updatedAt` back as `expectedUpdatedAt`. A save made in between is then refused with 409 `version_conflict` rather than overwritten, and nothing is written.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `name` (`string`): New name, trimmed, 1 to 120 characters.
- `description` (`string`): New note, at most 500 characters. Null clears it.
- `document` (`dictionary`): The new draft, whole, in the shape `GetAsync` returns. Every field carries all 15 of its keys, and a document needs exactly one email field, keyed `email` and required, and its field keys and ids must be unique.
- `settings.audienceIds` (`IEnumerable<string>`): The new list of audiences every sign-up joins, replacing the old one, at most 20 ids such as `aud_9f2c4b7e1a0d63d84c5f2e7b`. Every contact is in the default audience as well, so an empty list adds people there only. A new id that is not an audience the caller can reach is a 404 `audience_not_found`.
- `settings.doubleOptIn` (`bool`): Email each person a confirmation link and add them only once they open it. True needs `senderAddress`.
- `settings.senderAddress` (`string`): The workspace address confirmation emails come from, required for `doubleOptIn`. It has to be an address the caller may send as, or the call is refused with 422 `form_sender_refused`.
- `settings.confirmSubject` (`string`): The subject of the confirmation email, 1 to 200 characters.
- `settings.confirmMessage` (`string`): The text of the confirmation email above its button, at most 2000 characters.
- `settings.confirmButton` (`string`): The label of the confirmation button, 1 to 60 characters.
- `settings.successAction` (`string`): What a person sees after signing up: `message`, the thank-you copy in the document or `redirect`, which sends them to `redirectUrl`. `OpenEmail\Constants\FormSuccessActions` names them.
- `settings.redirectUrl` (`string`): The http or https page `redirect` sends people to, at most 2000 characters. Required while `successAction` is `redirect`.
- `settings.notifyAddresses` (`IEnumerable<string>`): Up to 10 addresses of this workspace that get an email about each sign-up, replacing the old list. Each has to be one the caller can read, or the call is refused with 422 `invalid_form`.
- `expectedUpdatedAt` (`string`): The `updatedAt` you read, as it came. When the form was saved since, nothing is written and the answer is 409 `version_conflict`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form as `GetAsync` returns it after the change, with a new `updatedAt`.

**Example**

```csharp
var form = await client.Forms.GetAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

var updated = await client.Forms.UpdateAsync(form["id"]!.GetValue<string>(), new Body
{
    ["settings"] = new Body { ["doubleOptIn"] = true, ["senderAddress"] = "hello@acme.com" },
    ["expectedUpdatedAt"] = form["updatedAt"],
});

Console.WriteLine($"{((bool?)updated["doubleOptIn"] == true ? "Double opt-in is on" : "Single opt-in")}");
```

**Notes**

- The checks `CreateAsync` makes apply to what you send: 422 `invalid_parameter`, `unknown_parameter`, `invalid_form`, `form_sender_refused` or `capability_unsupported`, and 404 `audience_not_found` on `settings.audienceIds`, each with `param` naming the field.
- Turning on `doubleOptIn`, naming a `senderAddress` or changing the confirmation email also needs `emails:send`, because the form then sends mail for you.
- Retried automatically on network failure and retryable statuses, since the same patch sent twice leaves the same form. With `expectedUpdatedAt`, a retry after a lost response can come back 409 `version_conflict` because the first attempt went through, so read the form before trying again.

Also available in: API [`PATCH /forms/{id}`](https://openemail.uk/docs/api/reference/forms#patch-forms-id); TypeScript [`forms.update()`](https://openemail.uk/docs/sdk/reference/forms#update); Python [`forms.update()`](https://openemail.uk/docs/python/reference/forms#update); Ruby [`forms.update`](https://openemail.uk/docs/ruby/reference/forms#update); PHP [`forms->update`](https://openemail.uk/docs/php/reference/forms#update); Go [`Forms.Update`](https://openemail.uk/docs/go/reference/forms#update); Java [`forms().update`](https://openemail.uk/docs/java/reference/forms#update); CLI [`openemail forms update`](https://openemail.uk/docs/cli/reference/forms#forms-update).

### `Forms.DeleteAsync`

Delete a form and every submission it holds

```csharp
Task<JsonObject> DeleteAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Deletes the form and every submission stored on it. Its hosted page and embed stop working at once, and `SubscribeAsync` answers 404 `form_not_found`. The people it added stay in your contacts and audiences.

There is no undo. To stop sign-ups and keep the form, its submissions and its statistics, use `PauseAsync`.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object` set to `form`, `id` and `deleted` set to true.

**Example**

```csharp
var removed = await client.Forms.DeleteAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

Console.WriteLine($"{removed["id"]}{((bool?)removed["deleted"] == true ? " is gone" : " is still there")}");
```

**Notes**

- An OAuth access token needs a verification code for this call, and is refused with 403 `step_up_required` until the app has verified one in the last 60 minutes. `IsStepUpRequired` on the error says so. An API key is never asked for a code.
- The SDK does not retry a delete. A 404 on your own second attempt after a lost response means the first one worked.

Also available in: API [`DELETE /forms/{id}`](https://openemail.uk/docs/api/reference/forms#delete-forms-id); TypeScript [`forms.delete()`](https://openemail.uk/docs/sdk/reference/forms#delete); Python [`forms.delete()`](https://openemail.uk/docs/python/reference/forms#delete); Ruby [`forms.delete`](https://openemail.uk/docs/ruby/reference/forms#delete); PHP [`forms->delete`](https://openemail.uk/docs/php/reference/forms#delete); Go [`Forms.Delete`](https://openemail.uk/docs/go/reference/forms#delete); Java [`forms().delete`](https://openemail.uk/docs/java/reference/forms#delete); CLI [`openemail forms delete`](https://openemail.uk/docs/cli/reference/forms#forms-delete).

### `Forms.PublishAsync`

Put the draft of a form live

```csharp
Task<JsonObject> PublishAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Copies the draft `document` to `publishedDocument` and sets the form `live`, so the hosted page, the embed and `SubscribeAsync` show and take the new version from then on. Publishing a paused form opens it again. It takes no body.

The draft is checked first: the document and settings have to pass the rules `CreateAsync` applies, and a double opt-in form needs a `senderAddress` that can send confirmations. An audience deleted since is skipped when people sign up. Submissions keep the answers, labels included, they were sent with, so publishing a change never rewrites them.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form as `GetAsync` returns it, with `status` `live`, a new `publishedAt` and `hasUnpublishedChanges` false.

**Example**

```csharp
var form = await client.Forms.PublishAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

Console.WriteLine($"{form["status"]} since {form["publishedAt"]} at {form["url"]}");
```

**Notes**

- A draft that breaks a rule is 422 `invalid_form`, and a double opt-in form that cannot send confirmations is 422 `form_sender_refused`, each with `param` naming the field. Publishing a double opt-in form also needs `emails:send`. A key or app limited to some addresses is refused with 422 `capability_unsupported` when the form's `settings.senderAddress` or `settings.notifyAddresses` falls outside them.
- Retried automatically on network failure and retryable statuses, since publishing the same draft twice leaves the same live form.

Also available in: API [`POST /forms/{id}/publish`](https://openemail.uk/docs/api/reference/forms#post-forms-id-publish); TypeScript [`forms.publish()`](https://openemail.uk/docs/sdk/reference/forms#publish); Python [`forms.publish()`](https://openemail.uk/docs/python/reference/forms#publish); Ruby [`forms.publish`](https://openemail.uk/docs/ruby/reference/forms#publish); PHP [`forms->publish`](https://openemail.uk/docs/php/reference/forms#publish); Go [`Forms.Publish`](https://openemail.uk/docs/go/reference/forms#publish); Java [`forms().publish`](https://openemail.uk/docs/java/reference/forms#publish); CLI [`openemail forms publish`](https://openemail.uk/docs/cli/reference/forms#forms-publish).

### `Forms.PauseAsync`

Stop a published form taking sign-ups

```csharp
Task<JsonObject> PauseAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Closes a published form. Its hosted page and embed stay up and show the closed title and message from its copy, and `SubscribeAsync` answers 409 `form_closed`. Pausing a paused form changes nothing. It takes no body.

The form keeps its documents, settings, submissions and statistics, and `ResumeAsync` opens it again with the version that was published last.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form as `GetAsync` returns it, with `status` `paused`.

**Example**

```csharp
var form = await client.Forms.PauseAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

Console.WriteLine($"{form["name"]} is {form["status"]}");
```

**Notes**

- A form that was never published cannot be paused, and is refused with 409 `form_not_published`.
- Retried automatically on network failure and retryable statuses, since a second pause changes nothing.

Also available in: API [`POST /forms/{id}/pause`](https://openemail.uk/docs/api/reference/forms#post-forms-id-pause); TypeScript [`forms.pause()`](https://openemail.uk/docs/sdk/reference/forms#pause); Python [`forms.pause()`](https://openemail.uk/docs/python/reference/forms#pause); Ruby [`forms.pause`](https://openemail.uk/docs/ruby/reference/forms#pause); PHP [`forms->pause`](https://openemail.uk/docs/php/reference/forms#pause); Go [`Forms.Pause`](https://openemail.uk/docs/go/reference/forms#pause); Java [`forms().pause`](https://openemail.uk/docs/java/reference/forms#pause); CLI [`openemail forms pause`](https://openemail.uk/docs/cli/reference/forms#forms-pause).

### `Forms.ResumeAsync`

Open a paused form again

```csharp
Task<JsonObject> ResumeAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Opens a paused form again with the version that was published last, so it takes sign-ups once more. Changes made to the draft since stay in the draft: `PublishAsync` puts them live and opens the form in one call. Resuming a live form changes nothing. It takes no body.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the form as `GetAsync` returns it, with `status` `live`.

**Example**

```csharp
var form = await client.Forms.ResumeAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

Console.WriteLine($"{form["status"]}{((bool?)form["hasUnpublishedChanges"] == true ? ", with draft edits still to publish" : "")}");
```

**Notes**

- A form that was never published is refused with 409 `form_not_published`. Publish it instead.
- The published version is checked again first, so a double opt-in form whose `senderAddress` can no longer send confirmations is refused with 422 `form_sender_refused`, and one that breaks a rule with 422 `invalid_form`. Resuming a double opt-in form also needs `emails:send`. A key or app limited to some addresses is refused with 422 `capability_unsupported` when the form's `settings.senderAddress` or `settings.notifyAddresses` falls outside them.
- Retried automatically on network failure and retryable statuses, since a second resume changes nothing.

Also available in: API [`POST /forms/{id}/resume`](https://openemail.uk/docs/api/reference/forms#post-forms-id-resume); TypeScript [`forms.resume()`](https://openemail.uk/docs/sdk/reference/forms#resume); Python [`forms.resume()`](https://openemail.uk/docs/python/reference/forms#resume); Ruby [`forms.resume`](https://openemail.uk/docs/ruby/reference/forms#resume); PHP [`forms->resume`](https://openemail.uk/docs/php/reference/forms#resume); Go [`Forms.Resume`](https://openemail.uk/docs/go/reference/forms#resume); Java [`forms().resume`](https://openemail.uk/docs/java/reference/forms#resume); CLI [`openemail forms resume`](https://openemail.uk/docs/cli/reference/forms#forms-resume).

### `Forms.DuplicateAsync`

Copy a form into a new draft

```csharp
Task<JsonObject> DuplicateAsync(
    string id,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Makes a new draft with the same document and settings, named after the original with copy added, such as Newsletter copy. Submissions and statistics are not copied, and the copy takes no sign-ups until you publish it. It takes no body.

An audience in the settings that the caller cannot reach is left out of the copy.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` for the new form, as `GetAsync` returns it, with its own `id`, `status` `draft`, `publishedDocument` null and empty `stats`.

**Example**

```csharp
var copy = await client.Forms.DuplicateAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9");

var renamed = await client.Forms.UpdateAsync(copy["id"]!.GetValue<string>(), new Body { ["name"] = "Spring launch" });

Console.WriteLine($"{renamed["id"]} {renamed["name"]} {renamed["status"]}");
```

**Notes**

- It counts toward the workspace limit of 100 forms, as `CreateAsync` does, and is refused with 422 `workspace_limit_reached` past it. A key or app limited to some addresses is refused with 422 `capability_unsupported` when the form's `settings.senderAddress` or `settings.notifyAddresses` falls outside them.
- Not retried automatically, since a retry after a lost response would make a second copy. List the forms and delete the spare.

Also available in: API [`POST /forms/{id}/duplicate`](https://openemail.uk/docs/api/reference/forms#post-forms-id-duplicate); TypeScript [`forms.duplicate()`](https://openemail.uk/docs/sdk/reference/forms#duplicate); Python [`forms.duplicate()`](https://openemail.uk/docs/python/reference/forms#duplicate); Ruby [`forms.duplicate`](https://openemail.uk/docs/ruby/reference/forms#duplicate); PHP [`forms->duplicate`](https://openemail.uk/docs/php/reference/forms#duplicate); Go [`Forms.Duplicate`](https://openemail.uk/docs/go/reference/forms#duplicate); Java [`forms().duplicate`](https://openemail.uk/docs/java/reference/forms#duplicate); CLI [`openemail forms duplicate`](https://openemail.uk/docs/cli/reference/forms#forms-duplicate).

### `Forms.AnalyticsAsync`

Read views, sign-ups and people added over a window

```csharp
Task<JsonObject> AnalyticsAsync(
    string id,
    int? days = null,
    int? minutes = null,
    string? grain = null,
    int? offsetMinutes = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns the numbers behind a form's Analytics tab in one request: `views`, `submissions` and `added` over a window, cut into buckets, with totals and the conversion rate.

A view is one load of the hosted page or the embed while the form is live, and views are not deduplicated by visitor. A submission is counted when it is stored, waiting or added. `added` counts the sign-ups added to the audiences in the window, one per sign-up, dated when they joined, so a double opt-in sign-up can be submitted in one bucket and added in a later one.

`series` is sparse and oldest first: a bucket with nothing in it has no entry, so a chart must fill the gaps. `grain:` sets the bucket width and the key shape, `YYYY-MM-DD`, `YYYY-MM-DDTHH` or `YYYY-MM-DDTHH:MM`, and `offsetMinutes:` shifts the boundaries so days break where the reader's day does. The window starts at the beginning of its oldest bucket, reported as `since`, and ends now, reported as `until`.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `days` (`int?`): How far back to look, from 1 to 1095, defaulting to 30.
- `minutes` (`int?`): The window in minutes, from 1 to 1576800, which wins over `days` when both are sent.
- `grain` (`string?`): Bucket width: `minute`, `hour` or `day`, defaulting to `day`.
- `offsetMinutes` (`int?`): Minutes east of UTC to cut the buckets in, from -840 to 840, defaulting to 0. Pass `(int)TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).TotalMinutes` for the zone of the machine.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object` set to `form_analytics`, `formId`, `since`, `until`, `grain`, `offsetMinutes`, `totals` and `series`. `totals` has `views`, `submissions`, `added`, `pending`, `lastSubmittedAt` and `conversion`, and each entry of `series` is an object with `bucket`, `views`, `submissions` and `added`.

**Example**

```csharp
var offsetMinutes = (int)TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now).TotalMinutes;
var analytics = await client.Forms.AnalyticsAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", days: 90, grain: "day", offsetMinutes: offsetMinutes);
var totals = analytics["totals"];

Console.WriteLine($"{totals?["views"]} views, {totals?["submissions"]} sign-ups, conversion {totals?["conversion"]?.ToString() ?? "none"}");

foreach (var bucket in analytics["series"]?.AsArray() ?? [])
{
    Console.WriteLine($"{bucket?["bucket"]}: {bucket?["added"]} added");
}
```

**Notes**

- `totals.conversion` is submissions divided by views over the window, at most 1, and null when the window has no views. `totals.pending` counts the submissions made in the window that are still waiting, and `totals.lastSubmittedAt` is the latest sign-up ever, inside the window or not.
- Views are counted by the hour, so with `grain: 'minute'` each hour's views fall in the bucket at the start of that hour.
- Read only, so the SDK retries it after a network failure like any other read.

Also available in: API [`GET /forms/{id}/analytics`](https://openemail.uk/docs/api/reference/forms#get-forms-id-analytics); TypeScript [`forms.analytics()`](https://openemail.uk/docs/sdk/reference/forms#analytics); Python [`forms.analytics()`](https://openemail.uk/docs/python/reference/forms#analytics); Ruby [`forms.analytics`](https://openemail.uk/docs/ruby/reference/forms#analytics); PHP [`forms->analytics`](https://openemail.uk/docs/php/reference/forms#analytics); Go [`Forms.Analytics`](https://openemail.uk/docs/go/reference/forms#analytics); Java [`forms().analytics`](https://openemail.uk/docs/java/reference/forms#analytics); CLI [`openemail forms analytics`](https://openemail.uk/docs/cli/reference/forms#forms-analytics).

### `Forms.ListSubmissionsAsync`

List one page of a form's submissions

```csharp
Task<Page> ListSubmissionsAsync(
    string id,
    string? q = null,
    string? status = null,
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns one page of the sign-ups a form has stored, newest first. Each keeps the answers as they were sent, labels included, so it still reads right after the form changes. Paging is keyset: `limit:` takes 1 to 100 and defaults to 25, and `nextCursor`, a submission id, goes back as `cursor:` while `hasMore` is true. `ListAllSubmissionsAsync` and `IterateSubmissionsAsync` do that walk for you.

`status` is `added` once the person is a contact in the audiences, and `pending` while a double opt-in form waits for them to confirm. `expired` is true on a pending submission whose newest confirmation link, sent at sign-up or by a resend, is more than 7 days old: approve it with `ApproveSubmissionAsync`, or send a fresh link with `ResendConfirmationAsync`.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `q` (`string?`): Only submissions whose email address contains this text, ignoring case, at most 200 characters.
- `status` (`string?`): Only `pending` or only `added` submissions, as in `OpenEmail\Constants\FormSubmissionStatuses`.
- `limit` (`int?`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`string?`): The `nextCursor` from the previous page, a submission id of this form. One that names no submission of this form is a 400 `invalid_cursor`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `Page` with `items`, `hasMore` and `nextCursor`. Each item has `id`, `formId`, `email`, `status`, `expired`, `answers`, `audienceIds`, `sourceUrl`, `confirmedAt` and `createdAt`, and each answer is an object with `fieldId`, `key`, `label`, `type`, `value` and `display`.

**Example**

```csharp
using OpenEmail.Constants;

var page = await client.Forms.ListSubmissionsAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", status: FormSubmissionStatuses.Pending);

foreach (var submission in page)
{
    Console.WriteLine($"{submission["email"]}{((bool?)submission["expired"] == true ? " (link expired)" : " (waiting)")}");
}
```

**Notes**

- `value` on an answer is text for most fields, a list of option values for a multiple choice or audience field, and true or false for a checkbox or consent field. `display` is the same answer as a person reads it, option labels included.
- `sourceUrl` is the page the form was filled in on, its origin and path only and at most 500 characters, when it was known.

Also available in: API [`GET /forms/{id}/submissions`](https://openemail.uk/docs/api/reference/forms#get-forms-id-submissions); TypeScript [`forms.listSubmissions()`](https://openemail.uk/docs/sdk/reference/forms#listSubmissions); Python [`forms.list_submissions()`](https://openemail.uk/docs/python/reference/forms#listSubmissions); Ruby [`forms.list_submissions`](https://openemail.uk/docs/ruby/reference/forms#listSubmissions); PHP [`forms->listSubmissions`](https://openemail.uk/docs/php/reference/forms#listSubmissions); Go [`Forms.ListSubmissions`](https://openemail.uk/docs/go/reference/forms#listSubmissions); Java [`forms().listSubmissions`](https://openemail.uk/docs/java/reference/forms#listSubmissions); CLI [`openemail forms list-submissions`](https://openemail.uk/docs/cli/reference/forms#forms-list-submissions).

### `Forms.ListAllSubmissionsAsync`

Collect every submission of a form into one object

```csharp
Task<IReadOnlyList<JsonObject>> ListAllSubmissionsAsync(
    string id,
    string? q = null,
    string? status = null,
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Follows `nextCursor` from page to page and returns every submission of the form that matches `q:` and `status:`, newest first. `limit:` sets the page size of each request, not the total.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `q` (`string?`): Only submissions whose email address contains this text, ignoring case, at most 200 characters.
- `status` (`string?`): Only `pending` or only `added` submissions, as in `OpenEmail\Constants\FormSubmissionStatuses`.
- `limit` (`int?`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`string?`): A submission id to start after.
- `apiKey` (`string?`): Overrides the client's API key for every page of this walk.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A list of `JsonObject` items holding every matching submission across all pages, each shaped like an item of `ListSubmissionsAsync`.

**Example**

```csharp
using OpenEmail.Constants;

var added = await client.Forms.ListAllSubmissionsAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", status: FormSubmissionStatuses.Added, limit: 100);

Console.WriteLine($"{string.Join(Environment.NewLine, added.Select(row => row?["email"]))}");
```

**Notes**

- A failure on any page throws out of the whole call. For a large form, `IterateSubmissionsAsync` holds one page in memory at a time.

Also available in: API [`GET /forms/{id}/submissions`](https://openemail.uk/docs/api/reference/forms#get-forms-id-submissions); TypeScript [`forms.listAllSubmissions()`](https://openemail.uk/docs/sdk/reference/forms#listAllSubmissions); Python [`forms.list_all_submissions()`](https://openemail.uk/docs/python/reference/forms#listAllSubmissions); Ruby [`forms.list_all_submissions`](https://openemail.uk/docs/ruby/reference/forms#listAllSubmissions); PHP [`forms->listAllSubmissions`](https://openemail.uk/docs/php/reference/forms#listAllSubmissions); Go [`Forms.ListAllSubmissions`](https://openemail.uk/docs/go/reference/forms#listAllSubmissions); Java [`forms().listAllSubmissions`](https://openemail.uk/docs/java/reference/forms#listAllSubmissions).

### `Forms.IterateSubmissionsAsync`

Stream a form's submissions one at a time

```csharp
IAsyncEnumerable<JsonObject> IterateSubmissionsAsync(
    string id,
    string? q = null,
    string? status = null,
    int? limit = null,
    string? cursor = null,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns an `IAsyncEnumerable<JsonObject>` that yields submissions one by one, newest first, and requests the next page only once the current one is drained. Breaking out of the loop stops the requests.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `q` (`string?`): Only submissions whose email address contains this text, ignoring case, at most 200 characters.
- `status` (`string?`): Only `pending` or only `added` submissions, as in `OpenEmail\Constants\FormSubmissionStatuses`.
- `limit` (`int?`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`string?`): A submission id to start after.
- `apiKey` (`string?`): Overrides the client's API key for every page of this walk.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

An `IAsyncEnumerable<JsonObject>` that yields one submission per step.

**Example**

```csharp
await foreach (var submission in client.Forms.IterateSubmissionsAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", q: "acme.com"))
{
    Console.WriteLine($"{submission["email"]} {submission["createdAt"]}");
}
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in: API [`GET /forms/{id}/submissions`](https://openemail.uk/docs/api/reference/forms#get-forms-id-submissions); TypeScript [`forms.iterateSubmissions()`](https://openemail.uk/docs/sdk/reference/forms#iterateSubmissions); Python [`forms.iterate_submissions()`](https://openemail.uk/docs/python/reference/forms#iterateSubmissions); Ruby [`forms.iterate_submissions`](https://openemail.uk/docs/ruby/reference/forms#iterateSubmissions); PHP [`forms->iterateSubmissions`](https://openemail.uk/docs/php/reference/forms#iterateSubmissions); Go [`Forms.IterateSubmissions`](https://openemail.uk/docs/go/reference/forms#iterateSubmissions); Java [`forms().iterateSubmissions`](https://openemail.uk/docs/java/reference/forms#iterateSubmissions).

### `Forms.GetSubmissionAsync`

Retrieve one submission with every answer

```csharp
Task<JsonObject> GetSubmissionAsync(
    string id,
    string submissionId,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Returns one submission of the form: the address the person signed up with, lower cased, its `status`, every answer as it was sent, the audiences it joins, the page it came from, and when it was made and confirmed.

Scopes: `forms:read`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `submissionId` (`string`, required): Submission id such as `fsb_3c7e1a9f0b2d4c6e8a1f3b5d`, from `ListSubmissionsAsync` or a `form.submitted` webhook.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `id`, `formId`, `email`, `status`, `expired`, `answers`, `audienceIds`, `sourceUrl`, `confirmedAt` and `createdAt`.

**Example**

```csharp
var submission = await client.Forms.GetSubmissionAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", "fsb_3c7e1a9f0b2d4c6e8a1f3b5d");

foreach (var answer in submission["answers"]?.AsArray() ?? [])
{
    Console.WriteLine($"{answer?["label"]}: {answer?["display"]}");
}
```

**Notes**

- A form you cannot reach is a 404 `form_not_found`, and a submission that is not on this form is a 404 `form_submission_not_found`.
- `confirmedAt` is when the person joined the audiences, and null while the submission is pending.

Also available in: API [`GET /forms/{id}/submissions/{submissionId}`](https://openemail.uk/docs/api/reference/forms#get-forms-id-submissions-submissionid); TypeScript [`forms.getSubmission()`](https://openemail.uk/docs/sdk/reference/forms#getSubmission); Python [`forms.get_submission()`](https://openemail.uk/docs/python/reference/forms#getSubmission); Ruby [`forms.get_submission`](https://openemail.uk/docs/ruby/reference/forms#getSubmission); PHP [`forms->getSubmission`](https://openemail.uk/docs/php/reference/forms#getSubmission); Go [`Forms.GetSubmission`](https://openemail.uk/docs/go/reference/forms#getSubmission); Java [`forms().getSubmission`](https://openemail.uk/docs/java/reference/forms#getSubmission); CLI [`openemail forms get-submission`](https://openemail.uk/docs/cli/reference/forms#forms-get-submission).

### `Forms.DeleteSubmissionAsync`

Delete one submission of a form

```csharp
Task<JsonObject> DeleteSubmissionAsync(
    string id,
    string submissionId,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Deletes the submission. The person it added stays in your contacts and audiences, and a pending one can no longer be confirmed, since its link stops working. There is no undo.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `submissionId` (`string`, required): Submission id such as `fsb_3c7e1a9f0b2d4c6e8a1f3b5d`, from `ListSubmissionsAsync` or a `form.submitted` webhook.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object` set to `form_submission`, `id`, `formId` and `deleted` set to true.

**Example**

```csharp
var removed = await client.Forms.DeleteSubmissionAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", "fsb_3c7e1a9f0b2d4c6e8a1f3b5d");

Console.WriteLine($"{removed["id"]} deleted from {removed["formId"]}");
```

**Notes**

- To take the person off your lists as well, remove them from the audience with `Audiences.RemoveContactAsync`, or delete the contact with `Contacts.DeleteAsync`.
- The SDK does not retry a delete. A 404 `form_submission_not_found` on your own second attempt after a lost response means the first one worked.

Also available in: API [`DELETE /forms/{id}/submissions/{submissionId}`](https://openemail.uk/docs/api/reference/forms#delete-forms-id-submissions-submissionid); TypeScript [`forms.deleteSubmission()`](https://openemail.uk/docs/sdk/reference/forms#deleteSubmission); Python [`forms.delete_submission()`](https://openemail.uk/docs/python/reference/forms#deleteSubmission); Ruby [`forms.delete_submission`](https://openemail.uk/docs/ruby/reference/forms#deleteSubmission); PHP [`forms->deleteSubmission`](https://openemail.uk/docs/php/reference/forms#deleteSubmission); Go [`Forms.DeleteSubmission`](https://openemail.uk/docs/go/reference/forms#deleteSubmission); Java [`forms().deleteSubmission`](https://openemail.uk/docs/java/reference/forms#deleteSubmission); CLI [`openemail forms delete-submission`](https://openemail.uk/docs/cli/reference/forms#forms-delete-submission).

### `Forms.DeleteSubmissionsAsync`

Delete up to 200 submissions of a form in one call

```csharp
Task<JsonObject> DeleteSubmissionsAsync(
    string id,
    IEnumerable<string> submissionIds,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Deletes many submissions of one form and answers with how many went. A repeated id counts once, and an id that names no submission of this form is skipped rather than refused. The people the submissions added stay in your contacts and audiences, and a pending one can no longer be confirmed.

Scopes: `forms:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `submissionIds` (`IEnumerable<string>`, required): From 1 to 200 submission ids of this form, such as `fsb_3c7e1a9f0b2d4c6e8a1f3b5d`, sent as `ids`. An empty list or more than 200 is a 422 `invalid_parameter` on `ids`.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object` set to `form_submission_batch`, `formId` and `deleted`, which counts the submissions this call removed.

**Example**

```csharp
var result = await client.Forms.DeleteSubmissionsAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", new[] { "fsb_3c7e1a9f0b2d4c6e8a1f3b5d", "fsb_9a4c2e7f1b3d5a6c8e0f2b4d" });

Console.WriteLine($"{result["deleted"]} submissions deleted");
```

**Notes**

- Send a longer list in chunks of 200.
- Not retried automatically. A retry after a lost response skips the submissions the first call removed, so its `deleted` can read 0.

Also available in: API [`POST /forms/{id}/submissions/batch-remove`](https://openemail.uk/docs/api/reference/forms#post-forms-id-submissions-batch-remove); TypeScript [`forms.deleteSubmissions()`](https://openemail.uk/docs/sdk/reference/forms#deleteSubmissions); Python [`forms.delete_submissions()`](https://openemail.uk/docs/python/reference/forms#deleteSubmissions); Ruby [`forms.delete_submissions`](https://openemail.uk/docs/ruby/reference/forms#deleteSubmissions); PHP [`forms->deleteSubmissions`](https://openemail.uk/docs/php/reference/forms#deleteSubmissions); Go [`Forms.DeleteSubmissions`](https://openemail.uk/docs/go/reference/forms#deleteSubmissions); Java [`forms().deleteSubmissions`](https://openemail.uk/docs/java/reference/forms#deleteSubmissions); CLI [`openemail forms delete-submissions`](https://openemail.uk/docs/cli/reference/forms#forms-delete-submissions).

### `Forms.ApproveSubmissionAsync`

Add a waiting sign-up without its confirmation

```csharp
Task<JsonObject> ApproveSubmissionAsync(
    string id,
    string submissionId,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Adds the person behind a pending submission to its audiences without waiting for them to open the confirmation email, for when you know them. The submission comes back `added` with `confirmedAt` set, and a `form.confirmed` webhook goes out with `via` set to `approval`. It takes no body.

It does not resubscribe someone who unsubscribed from one of the audiences, as their own confirmation would. A submission that is already added is returned as it is.

Scopes: `forms:write`, `contacts:write`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `submissionId` (`string`, required): Submission id such as `fsb_3c7e1a9f0b2d4c6e8a1f3b5d`, from `ListSubmissionsAsync` or a `form.submitted` webhook.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject`, the submission as `GetSubmissionAsync` returns it, now with `status` `added` and `confirmedAt` set.

**Example**

```csharp
var submission = await client.Forms.ApproveSubmissionAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", "fsb_3c7e1a9f0b2d4c6e8a1f3b5d");

Console.WriteLine($"{submission["email"]} {submission["status"]} at {submission["confirmedAt"]}");
```

**Notes**

- It needs `contacts:write` as well as `forms:write`, since it adds a contact. A key without both is refused with 403 `insufficient_scope`.
- Retried automatically on network failure and retryable statuses, since approving an added submission changes nothing.

Also available in: API [`POST /forms/{id}/submissions/{submissionId}/approve`](https://openemail.uk/docs/api/reference/forms#post-forms-id-submissions-submissionid-approve); TypeScript [`forms.approveSubmission()`](https://openemail.uk/docs/sdk/reference/forms#approveSubmission); Python [`forms.approve_submission()`](https://openemail.uk/docs/python/reference/forms#approveSubmission); Ruby [`forms.approve_submission`](https://openemail.uk/docs/ruby/reference/forms#approveSubmission); PHP [`forms->approveSubmission`](https://openemail.uk/docs/php/reference/forms#approveSubmission); Go [`Forms.ApproveSubmission`](https://openemail.uk/docs/go/reference/forms#approveSubmission); Java [`forms().approveSubmission`](https://openemail.uk/docs/java/reference/forms#approveSubmission); CLI [`openemail forms approve-submission`](https://openemail.uk/docs/cli/reference/forms#forms-approve-submission).

### `Forms.ResendConfirmationAsync`

Email a waiting sign-up a fresh confirmation link

```csharp
Task<JsonObject> ResendConfirmationAsync(
    string id,
    string submissionId,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Emails the person behind a pending submission a new confirmation link from the form's `senderAddress`, with the form's current subject, message and button. The new link works for 7 days from now. It takes no body.

To protect the person, one address gets at most one confirmation from a form every ten minutes, and five a day across the workspace. A call within ten minutes of the last one, or past the fifth in a day, sends nothing and answers `confirmationSent: false`, and so does a call for a submission that is already added.

Scopes: `forms:write`, `emails:send`.

**Parameters**

- `id` (`string`, required): Form id such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `submissionId` (`string`, required): Submission id such as `fsb_3c7e1a9f0b2d4c6e8a1f3b5d`, from `ListSubmissionsAsync` or a `form.submitted` webhook.
- `apiKey` (`string?`): Overrides the client's API key for this call only.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with the fields `GetSubmissionAsync` returns plus `confirmationSent`, true when an email went out on this call.

**Example**

```csharp
var result = await client.Forms.ResendConfirmationAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", "fsb_3c7e1a9f0b2d4c6e8a1f3b5d");

Console.WriteLine($"{((bool?)result["confirmationSent"] == true ? "A fresh link is on its way" : "Nothing sent, try again later")}");
```

**Notes**

- Needs `emails:send` as well as `forms:write`, because the call sends an email.
- When confirmations cannot go out from the form's address, or it has none, the call is refused with 422 `form_sender_refused` on `settings.senderAddress`.
- `expired` follows the newest confirmation link, so a resend that went out turns it false again for the next 7 days.
- Not retried automatically, since a retry could send a second email. Read `confirmationSent` rather than calling again at once.

Also available in: API [`POST /forms/{id}/submissions/{submissionId}/resend`](https://openemail.uk/docs/api/reference/forms#post-forms-id-submissions-submissionid-resend); TypeScript [`forms.resendConfirmation()`](https://openemail.uk/docs/sdk/reference/forms#resendConfirmation); Python [`forms.resend_confirmation()`](https://openemail.uk/docs/python/reference/forms#resendConfirmation); Ruby [`forms.resend_confirmation`](https://openemail.uk/docs/ruby/reference/forms#resendConfirmation); PHP [`forms->resendConfirmation`](https://openemail.uk/docs/php/reference/forms#resendConfirmation); Go [`Forms.ResendConfirmation`](https://openemail.uk/docs/go/reference/forms#resendConfirmation); Java [`forms().resendConfirmation`](https://openemail.uk/docs/java/reference/forms#resendConfirmation); CLI [`openemail forms resend-confirmation`](https://openemail.uk/docs/cli/reference/forms#forms-resend-confirmation).

### `Forms.SubscribeAsync`

Sign someone up through a published form

```csharp
Task<JsonObject> SubscribeAsync(
    string formId,
    IReadOnlyDictionary<string, object?> values,
    string? apiKey = null,
    CancellationToken cancellationToken = default)
```

Posts a sign-up to a published form, as a visitor of its hosted page does, and answers with the outcome. It sends no credential, even from a client that holds one, so it works for any published form, in this workspace or another.

`values` holds the answers keyed by field key, and every form has the field keyed `email`. A text field takes a string, a number field a number or a numeric string, a checkbox or consent field true or false, a dropdown or single choice field one option value, and a multiple choice or audience field an object of option values. A key the form does not have is ignored, and a key starting `oe_` is never stored as an answer.

On a double opt-in form `outcome` is `pending`: the person joins the audiences once they open a confirmation link, emailed after this answer unless this form emailed that address in the last ten minutes or the address has had five from this workspace today. Otherwise they join at once and `outcome` is `added`. Someone who unsubscribed from an audience stays unsubscribed unless they confirm through a double opt-in form.

Sends no credential.

**Parameters**

- `formId` (`string`, required): The id of a published form, such as `frm_8d2f6a1c9b3e47d0a5f1c2e9`.
- `email` (`string`, required): The address to sign up, the answer to the email field every form has.
- `oe_source` (`string`): The page the form was filled in on, stored as its origin and path, at most 500 characters. A call from a server has no `Referer` to fall back on, so pass it to keep the source.
- `apiKey` (`string?`): Ignored and never sent. Signing up needs no credential.
- `cancellationToken` (`CancellationToken`): Cancels the request.

**Returns**

A `JsonObject` with `object` set to `form_subscription`, `formId`, `outcome` and `redirectUrl`. `outcome` is `added` or `pending`, and `redirectUrl` is where the form sends people after a sign-up when it is set to redirect, otherwise null.

**Example**

```csharp
try
{
    var result = await client.Forms.SubscribeAsync("frm_8d2f6a1c9b3e47d0a5f1c2e9", new Body
    {
        ["email"] = "ada@example.com",
        ["first_name"] = "Ada",
        ["topics"] = new[] { "product", "events" },
        ["oe_source"] = "https://acme.com/launch",
    });

    Console.WriteLine($"{result["outcome"]}");
}
catch (OpenEmailApiException error)
{
    if (error.Code != "invalid_form_submission")
    {
        throw;
    }
}
```

**Notes**

- An answer that is missing or not valid is a 422 `invalid_form_submission`, and nothing is stored. The exception's `fields` is a list with one object per problem, each with `key` and `error`, such as `new Body { ["key"] = "email", ["error"] = "email" }`, and its `body` holds the whole response. A form that was never published is a 404 `form_not_found`, a paused one a 409 `form_closed`.
- Every post to any of the workspace's forms counts toward a limit of 40 every 10 minutes from one network, whatever the outcome, counted by the address the request comes from, so a server that relays sign-ups for many people shares one allowance. Past it the call is refused with 429 `form_rate_limited`.
- Leave out `oe_started`, and send `oe_website` empty or not at all: they catch bots. A sign-up that fills `oe_website`, or carries an `oe_started` token that is not valid or is under 1.5 seconds old, gets a normal answer and is dropped.
- Not retried automatically, since a retry after a lost response would store a second submission on a single opt-in form. On a double opt-in form, signing up again before confirming updates the waiting submission instead, and `form.submitted` fires again only when the answers changed.

Also available in: API [`POST /subscribe/{formId}`](https://openemail.uk/docs/api/reference/forms#post-subscribe-formid); TypeScript [`forms.subscribe()`](https://openemail.uk/docs/sdk/reference/forms#subscribe); Python [`forms.subscribe()`](https://openemail.uk/docs/python/reference/forms#subscribe); Ruby [`forms.subscribe`](https://openemail.uk/docs/ruby/reference/forms#subscribe); PHP [`forms->subscribe`](https://openemail.uk/docs/php/reference/forms#subscribe); Go [`Forms.Subscribe`](https://openemail.uk/docs/go/reference/forms#subscribe); Java [`forms().subscribe`](https://openemail.uk/docs/java/reference/forms#subscribe); CLI [`openemail forms subscribe`](https://openemail.uk/docs/cli/reference/forms#forms-subscribe).
