---
title: Forms & Content
description: Submitting to a shop's contact forms, and reading the small set of content pages headless shops can still show.
---

## Forms

`storefront.forms` lets a headless shop submit to a Bolder contact form (the same forms
you build in the shop admin) without any Bolder-hosted markup. Useful for a contact page
or feedback form on a statically-generated site, where the form's HTML needs to be part
of the static page for SEO but the submission still needs to reach the shop's contacts.

### get(slug, fn)

Fetches a form's field definitions so you can render your own inputs.

```js
storefront.forms.get('contact', (err, form) => {
  if (err) return console.error(err)

  console.log(form.title, form.message) // form.message is the form's body/intro text
  form.contactFields.forEach(field => {
    // field: { key, name, field_type, required, value, options? }
  })
  form.customFields.forEach(field => { /* same shape */ })
})
```

### submit(form, data, fn)

Submits data collected from your own form UI. Pass the `form` object returned by `get()`
— `submit` uses it to sort your flat `data` object into the `contact_fields`/
`custom_fields` split the backend expects, keyed by each field's `key`.

```js
storefront.forms.get('contact', (err, form) => {
  myForm.addEventListener('submit', e => {
    e.preventDefault()

    storefront.forms.submit(form, {
      name: myForm.name.value,
      email: myForm.email.value,
      message: myForm.message.value, // a custom field, e.g.
    }, (err, result) => {
      if (err) return showErrors(err.errors)
      showSuccess(form.sentMessageText)
    })
  })
})
```

On validation failure `err` is `{ success: false, errors: [{ field, messages }] }`.

## Content JSON endpoints

A headless shop 404s on every server-rendered page — there's no theme to render it with — except
for a small set of shop-authored/-configured content pages that are also served as plain JSON, so
a headless integration can still show them: `storefront.promos`, plus `storefront.plans` and
`storefront.points` when the shop has them enabled.

```js
storefront.promos.get(function(err, promos) { renderPromos(promos) })
```

| Endpoint | SDK | Mirrors | Returns |
|----------|-----|---------|---------|
| `GET /plans.json` | `storefront.plans.get(fn)` | `/plans` | Array of membership plans: `{ id, name, description, url, images, attributes, subscription_fees }`. Each `subscription_fees` entry has `{ id, setup_fee, renewal_fee, payment_interval, interval_label, variant_id }` (amounts are numeric, smallest-currency-unit). |
| `GET /promos.json` | `storefront.promos.get(fn)` | `/promos` | Array of the shop's visible/eligible promotions — same shape as `GET /promos/:id.json`: `{ id, discount, discount_type, name, automatic, full_discount, ... }`. |
| `GET /policies/points.json` | `storefront.points.get(fn)` | `/policies/points` | The shop's points-club policy config (naming, earn/expiration rules, an example earn calculation, contact channel) — not a customer's balance, see `buyer_points` under [Customer Console](/frontend/sdk/console) for that. `{}` if the shop has the points-club addon but no settings configured. |

`storefront.plans` and `storefront.points` only exist when `/shop.json` reports the shop has them
enabled (`plans_enabled`/`points_enabled`) — a shop without membership plans has no
`storefront.plans` at all, rather than one that 404s when called. `storefront.promos` is always
present. All three are unauthenticated, public reads — no `storefront.authAjax` needed.
