# Contact & Subscribe Forms (/developers/crm-intake)



The ResytechApi client exposes two public intake endpoints for your marketing site:

* **`api.subscribeForm.subscribe(...)`** — email-only mailing-list signup. Creates a Contact with `obtain_method = subscribe_form` and `contact_consent = true`.
* **`api.contactForm.submit(...)`** — full contact / lead-capture form. Creates a Contact and sends a notification email to the operator's support address.

Both endpoints are **public** (no booking session required) and both are protected on the server with **Google reCAPTCHA v3**. You need to configure a reCAPTCHA key pair before either form will accept submissions in production.

Prerequisites: Google reCAPTCHA v3 [#prerequisites-google-recaptcha-v3]

1\. Generate keys in the Google reCAPTCHA admin [#1-generate-keys-in-the-google-recaptcha-admin]

1. Sign in to the [Google reCAPTCHA admin console](https://www.google.com/recaptcha/admin/create).
2. Pick a **Label** (e.g. *My Marketing Site*).
3. Choose **reCAPTCHA v3**.
4. Add every domain that will embed your form, *without* `https://` and *without* paths. For example:
   * `yourbusiness.com`
   * `www.yourbusiness.com`
   * `staging.yourbusiness.com`
5. Accept the terms and click **Submit**.

Google returns two values:

| Key            | Where it's used                                                                | Sensitivity                              |
| -------------- | ------------------------------------------------------------------------------ | ---------------------------------------- |
| **Site key**   | Embedded in your page HTML and passed to `grecaptcha.execute()` in the browser | Public — safe to commit                  |
| **Secret key** | Pasted into the Resytech dashboard so the server can verify tokens             | **Secret** — never expose to the browser |

2\. Store the secret key in the Resytech dashboard [#2-store-the-secret-key-in-the-resytech-dashboard]

In the Resytech dashboard, navigate to **CRM > Settings** and paste the secret key into the **reCAPTCHA Secret** field, then save.

* The page shows **Configured** / **Not configured** rather than the value itself — the secret is never returned by the API once stored.
* Use the **Clear stored secret** action to remove it (e.g. if you're rotating keys).
* A location with no secret configured will reject every contact / subscribe submission with `"Resytech Location Not Setup"`.

> One reCAPTCHA key pair per Resytech **Location**. If you run multiple locations and want them to share a key, paste the same secret into each location's settings.

3\. Load reCAPTCHA on your page [#3-load-recaptcha-on-your-page]

Add the reCAPTCHA loader script and your **site key** to the page that hosts the form:

```html
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
```

Then, when the user submits the form, obtain a fresh token with `grecaptcha.execute()` and pass it to the API call. Tokens are **single-use** and expire after \~2 minutes — always generate one at submit time, never reuse one.

```javascript
const recaptchaSiteKey = 'YOUR_SITE_KEY';

async function getRecaptchaToken(action) {
  return new Promise((resolve, reject) => {
    grecaptcha.ready(() => {
      grecaptcha.execute(recaptchaSiteKey, { action }).then(resolve).catch(reject);
    });
  });
}
```

The `action` string is a label that Google uses for analytics and risk scoring. Pick a short, human-readable name per form — e.g. `subscribe`, `contact`.

***

SubscribeForm Controller [#subscribeform-controller]

`api.subscribeForm.subscribe(request)` adds an email address to the operator's CRM as a consented contact.

```javascript
const token = await getRecaptchaToken('subscribe');

const result = await api.subscribeForm.subscribe({
  email: 'jane@example.com',
  token,
  location: 'location-uuid'
});

if (result.success) {
  console.log('Subscribed!');
} else {
  console.error(result.message);
}
```

SubscribeFormRequest [#subscribeformrequest]

| Field      | Type     | Required | Description                                                       |
| ---------- | -------- | -------- | ----------------------------------------------------------------- |
| `email`    | `string` | Yes      | Email address to subscribe                                        |
| `token`    | `string` | Yes      | Single-use reCAPTCHA v3 token obtained via `grecaptcha.execute()` |
| `location` | `string` | Yes      | Location UUID (the operator/venue receiving the signup)           |

SubscribeFormResponse [#subscribeformresponse]

Extends `ApiResponseBase` — see [ResytechApi Overview](/developers/resytech-api#error-response-format) for the full shape. No additional fields.

***

ContactForm Controller [#contactform-controller]

`api.contactForm.submit(request)` records a full contact-form / lead-capture submission. The server creates a Contact record and emails the operator's support address with the submitted details.

```javascript
const token = await getRecaptchaToken('contact');

const result = await api.contactForm.submit({
  firstName: 'Jane',
  lastName: 'Doe',
  email: 'jane@example.com',
  phone: '555-123-4567',
  message: 'Hi, I have a question about your kayak tours.',
  token,
  location: 'location-uuid'
});

if (result.success) {
  console.log('Inquiry sent');
} else {
  console.error(result.message);
}
```

ContactFormRequest [#contactformrequest]

All fields are required by the server.

| Field       | Type     | Required | Description                   |
| ----------- | -------- | -------- | ----------------------------- |
| `firstName` | `string` | Yes      | Submitter's first name        |
| `lastName`  | `string` | Yes      | Submitter's last name         |
| `email`     | `string` | Yes      | Submitter's email address     |
| `phone`     | `string` | Yes      | Submitter's phone number      |
| `message`   | `string` | Yes      | Free-text message body        |
| `token`     | `string` | Yes      | Single-use reCAPTCHA v3 token |
| `location`  | `string` | Yes      | Location UUID                 |

ContactFormResponse [#contactformresponse]

Extends `ApiResponseBase`. No additional fields.

***

Full Example: Subscribe form on a marketing page [#full-example-subscribe-form-on-a-marketing-page]

```html
<!doctype html>
<html>
  <head>
    <script src="https://js.resytech.com/latest/resytech.js"></script>
    <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
  </head>
  <body>
    <form id="subscribe-form">
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
      <p id="status"></p>
    </form>

    <script>
      const api = new ResytechApi();
      const recaptchaSiteKey = 'YOUR_SITE_KEY';
      const locationUuid = 'YOUR_LOCATION_UUID';

      async function getRecaptchaToken(action) {
        return new Promise((resolve) => {
          grecaptcha.ready(() => {
            grecaptcha.execute(recaptchaSiteKey, { action }).then(resolve);
          });
        });
      }

      document.getElementById('subscribe-form').addEventListener('submit', async (e) => {
        e.preventDefault();
        const status = document.getElementById('status');
        const email = e.target.email.value;

        status.textContent = 'Subscribing...';

        const token = await getRecaptchaToken('subscribe');
        const result = await api.subscribeForm.subscribe({
          email,
          token,
          location: locationUuid
        });

        status.textContent = result.success
          ? 'Thanks! You\'re subscribed.'
          : `Sorry — ${result.message}`;
      });
    </script>
  </body>
</html>
```

The same pattern applies to `api.contactForm.submit(...)` — swap the form fields, the action label (`contact`), and the API call.

***

Troubleshooting [#troubleshooting]

| Symptom                                                                       | Likely cause                                                                                                                                                                                |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Response says &#x2A;"Resytech Location Not Setup"*                            | The Location's reCAPTCHA secret hasn't been saved in the dashboard yet. See **Step 2** above.                                                                                               |
| Response says &#x2A;"Captcha verification failed"* (or 403 in release builds) | The site key in your page doesn't match the secret saved in the dashboard, the token expired (>2 min old), or the token was already consumed. Always generate a fresh token at submit time. |
| Tokens are generated but the call still fails verification                    | Double-check that the domain hosting your form is listed in the reCAPTCHA admin console for this key. v3 rejects tokens from non-allow-listed origins.                                      |
| You want different secrets per environment                                    | Use one reCAPTCHA key pair per environment (production / staging) and save each Location's appropriate secret in its dashboard settings.                                                    |
