---
title: "Forms"
description: "`forms->list`, `listAll`, `iterate`, `get`, `create`, `design`, `redesign`, `update`, `delete`, `publish`, `pause`, `resume`, `duplicate`, `analytics`, `listStarters`, `getStarter`, `listSubmissions`, `getSubmission`, `deleteSubmission`, `deleteSubmissions`, `approveSubmission`, `resendConfirmation` and `subscribe`."
url: "https://openemail.uk/docs/php/forms"
area: "PHP"
category: "Mailbox"
---

# Forms

`forms->list`, `listAll`, `iterate`, `get`, `create`, `design`, `redesign`, `update`, `delete`, `publish`, `pause`, `resume`, `duplicate`, `analytics`, `listStarters`, `getStarter`, `listSubmissions`, `getSubmission`, `deleteSubmission`, `deleteSubmissions`, `approveSubmission`, `resendConfirmation` and `subscribe`.

## Every method

**forms.php**

```
use OpenEmail\Constants\FormStarterSlugs;
use OpenEmail\Constants\FormSubmissionStatuses;

$form = $client->forms->create([
    'name' => 'Newsletter sign-up',
    'starter' => FormStarterSlugs::NEWSLETTER,
    'settings' => ['audienceIds' => ['aud_4c1b8e2a7d9f05c36b4e8a71']],
    'publish' => true,
]);

echo $form['url'], ' ', $form['subscribeUrl'], PHP_EOL;

$saved = $client->forms->update($form['id'], [
    'settings' => ['doubleOptIn' => true, 'senderAddress' => 'news@acme.com'],
    'expectedUpdatedAt' => $form['updatedAt'],
]);

$signup = $client->forms->subscribe($form['id'], [
    'email' => 'ann@example.com',
    'first_name' => 'Ann',
    'consent' => true,
]);

foreach ($client->forms->iterateSubmissions($form['id'], status: FormSubmissionStatuses::PENDING) as $submission) {
    if ($submission['expired']) {
        $client->forms->resendConfirmation($form['id'], $submission['id']);
    }
}

$stats = $client->forms->analytics($form['id'], days: 30);
$starters = $client->forms->listStarters();

$client->forms->pause($form['id']);
$client->forms->resume($form['id']);
$copy = $client->forms->duplicate($form['id']);
$client->forms->delete($copy['id']);

echo $saved['senderIssue'] ?? 'ready to send', ' ', $signup['outcome'], ' ', $stats['totals']['conversion'] ?? 'no views yet', ' ', count($starters), PHP_EOL;
```

A form keeps a draft `document` and the `publishedDocument` visitors see. `update` changes the draft and the settings, and `publish` puts the draft live. Settings take effect at once, published or not, and `expectedUpdatedAt` refuses a save that would overwrite someone else’s with a 409 `version_conflict`, thrown as a `ConflictException`.

The fields of a form are the keys of one array under the API’s camelCase names (`expectedUpdatedAt`, `doubleOptIn`), while filters and options are named arguments (`status:` on `listSubmissions`, `offsetMinutes:` on `analytics`). A form comes back as an array keyed in camelCase, so `$form['subscribeUrl']` reads the address that takes sign-ups.

`design` builds a new draft form from a written brief, as Create with AI does on the Forms page, and `redesign` applies written instructions to the draft of a form, as Ask AI does in the form builder. Each spends one AI action and writes only the draft, so visitors see nothing new until `publish`.

Reading needs `forms:read` and changing needs `forms:write`. `approveSubmission` also needs `contacts:write`, because it adds a contact. `resendConfirmation` also needs `emails:send`, and so does a call that makes the form send mail: turning on `doubleOptIn`, setting `senderAddress` or the confirmation email, or publishing or resuming a double opt-in form. `delete` asks an OAuth access token for a verification code, and an API key never. Until the token has one, `delete` throws a `PermissionException` whose `isStepUpRequired()` is true.

> `subscribe` signs someone up as the form’s page does and sends no credential, even from a client that holds one, so `apiKey:` is ignored. The answers are its second argument, one array keyed by the form’s field keys. Every sign-up from one network shares a limit of 40 every ten minutes, so a server relaying sign-ups for many people reaches it quickly: add people you already know with `audiences->importContacts` instead. Past the limit the call throws a `RateLimitException`. Pass the page the form was on as `oe_source`, leave out `oe_started`, and send `oe_website` empty or not at all.

> A 422 from `subscribe` is `invalid_form_submission`, thrown as a `ValidationException`, and the exception’s `fields` lists each answer that is missing or not valid as an array with `key` and `error`, with reasons such as `required`, `email` and `option`. `OpenEmail\Constants\FormFieldErrors` names every reason. The package runs on a server. A browser posts the answers to the form’s `subscribeUrl` itself, as a JSON body or with an `Accept: application/json` header, and gets JSON back from any origin. Without either it gets a 303 redirect to the hosted page.

## Response: a form

`list` returns one `OpenEmail\Result\Page` of forms, newest first, without `document` and `settings`. `listAll` returns every form in one array, and `iterate` returns a `Generator` that walks every page one form at a time. `get`, `create`, `update`, `publish`, `pause`, `resume` and `duplicate` return the whole form as an array, which adds `document`, `publishedDocument`, `settings`, `audiences`, `senderIssue` and `senderProblem`.

- `id` (string): The durable handle, `frm_` followed by 24 hex characters.
- `status` (string): `draft` until the first publish, then `live` while it takes sign-ups and `paused` while it does not. A form never goes back to `draft`. `OpenEmail\Constants\FormStatuses` names the three.
- `url` (string): The hosted page of the published form, to share as a link.
- `subscribeUrl` (string): Where a plain HTML form, or a script in the browser, posts the answers.
- `document` (array): The draft: `fields` in order, the `copy` around them and the `style`.
- `publishedDocument` (array or null): What visitors see now, or null until the first publish.
- `settings` (array): Where sign-ups go and what happens after one: `audienceIds`, `doubleOptIn`, `senderAddress`, the confirmation email, `successAction`, `redirectUrl` and `notifyAddresses`.
- `hasUnpublishedChanges` (bool): True when the draft differs from what visitors see. Always false before the first publish.
- `senderIssue` (string or null): Why a double opt-in form cannot send its confirmation emails right now: `missing`, `not_sendable` or `not_allowed`. It is null when it can. `OpenEmail\Constants\FormSenderIssues` names the three.
- `stats` (array): `views`, `submissions`, `added`, `pending` and `lastSubmittedAt`, counted at the moment of the read.

## Submissions

`listSubmissions` pages newest first, with `q:` to search email addresses and `status:` for `pending` or `added`, and `listAllSubmissions` and `iterateSubmissions` walk every page. `OpenEmail\Constants\FormSubmissionStatuses` names the two statuses. Each submission is an array that keeps the answers as they were sent, labels included, so it still reads right after the form changes.

`resendConfirmation` returns the submission with `confirmationSent`. It is false when nothing went out: one address gets one confirmation per form every ten minutes and five a day across the workspace, and an added submission gets none. `expired` marks a pending sign-up whose latest link has run out.
