---
title: "Forms"
description: "`forms.list`, `get`, `create`, `update`, `delete`, `publish`, `pause`, `resume`, `duplicate`, `analytics`, `list_starters`, `get_starter`, `list_submissions`, `get_submission`, `delete_submission`, `delete_submissions`, `approve_submission`, `resend_confirmation` and `subscribe`."
url: "https://openemail.uk/docs/ruby/forms"
area: "Ruby"
category: "Mailbox"
---

# Forms

`forms.list`, `get`, `create`, `update`, `delete`, `publish`, `pause`, `resume`, `duplicate`, `analytics`, `list_starters`, `get_starter`, `list_submissions`, `get_submission`, `delete_submission`, `delete_submissions`, `approve_submission`, `resend_confirmation` and `subscribe`.

## Every method

**forms.rb**

```
form = client.forms.create(
  name: "Newsletter sign-up",
  starter: "newsletter",
  settings: {audienceIds: ["aud_4c1b8e2a7d9f05c36b4e8a71"]},
  publish: true
)

puts form[:url], form[:subscribeUrl]

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)

client.forms.iterate_submissions(form[:id], status: "pending") do |submission|
  client.forms.resend_confirmation(form[:id], submission[:id]) if submission[:expired]
end

stats = client.forms.analytics(form[:id], days: 30)
starters = client.forms.list_starters

client.forms.pause(form[:id])
client.forms.resume(form[:id])
copy = client.forms.duplicate(form[:id])
client.forms.delete(copy[:id])

puts saved[:senderIssue], signup[:outcome], stats.dig(:totals, :conversion), starters.size
```

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`, raised as `OpenEmail::ConflictError`.

The fields of a form keep the API’s camelCase names (`expectedUpdatedAt:`, `doubleOptIn`), passed as keyword arguments or one Hash, while filters and options are snake_case keywords (`status:` on `list_submissions`, `offset_minutes:` on `analytics`). A form comes back as a Hash with Symbol keys, so `form[:subscribeUrl]` reads the address that takes sign-ups.

Reading needs `forms:read` and changing needs `forms:write`. `approve_submission` also needs `contacts:write`, because it adds a contact. `resend_confirmation` 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` raises `OpenEmail::PermissionError` with `step_up_required?` true.

> `subscribe` signs someone up as the form’s page does and sends no credential, even from a client that holds one, so `api_key:` is ignored. The answers go in as keyword arguments or one Hash, 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.import_contacts` instead. Past the limit the call raises `OpenEmail::RateLimitError`. 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`, raised as `OpenEmail::ValidationError`, and the error’s `fields` lists each answer that is missing or not valid as a Hash with `key` and `error`, with reasons such as `required`, `email` and `option`. `OpenEmail::FORM_FIELD_ERRORS` names every reason. The gem 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::Page` of forms, newest first, without `document` and `settings`, and `list_all` and `iterate` walk every page. `get`, `create`, `update`, `publish`, `pause`, `resume` and `duplicate` return the whole form as a Hash, 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::FORM_STATUSES` 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` (Hash): The draft: `fields` in order, the `copy` around them and the `style`.
- `publishedDocument` (Hash or nil): What visitors see now, or nil until the first publish.
- `settings` (Hash): Where sign-ups go and what happens after one: `audienceIds`, `doubleOptIn`, `senderAddress`, the confirmation email, `successAction`, `redirectUrl` and `notifyAddresses`.
- `hasUnpublishedChanges` (Boolean): True when the draft differs from what visitors see. Always false before the first publish.
- `senderIssue` (String or nil): Why a double opt-in form cannot send its confirmation emails right now: `missing`, `not_sendable` or `not_allowed`. It is nil when it can. `OpenEmail::FORM_SENDER_ISSUES` names the three.
- `stats` (Hash): `views`, `submissions`, `added`, `pending` and `lastSubmittedAt`, counted at the moment of the read.

## Submissions

`list_submissions` pages newest first, with `q:` to search email addresses and `status:` for `pending` or `added`, and `list_all_submissions` and `iterate_submissions` walk every page. `OpenEmail::FORM_SUBMISSION_STATUSES` names the two statuses. Each submission is a Hash that keeps the answers as they were sent, labels included, so it still reads right after the form changes.

`resend_confirmation` 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.
