# CRM · Forms

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /crm/contact-form/post/{app}/{name}

**Submit a contact form (multipart)**

`operationId: CrmFormController_saveContactFormPost`

Public. Accepts a website contact form as `multipart/form-data`, so a plain HTML `<form method="post" enctype="multipart/form-data">` can post straight here. Uploaded files are stored under `form-data/{app}/{name}/` and replaced in the body by `{ originalName, mimeType, extension, size, path, signedUrl }`; the rest is handled as the JSON variant.

#### Signature

```http
POST /crm/contact-form/post/{app}/{name} (app: string, name: string, type?: string, body) -> The submission result
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /crm/contact-form/json/{app}/{name}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `app` | path | string | yes | Application the form belongs to. |
| `name` | path | string | yes | Form name. |
| `type` | query | string | — |  |

### Request body

The submitted values and files.

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The submission result |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/contact-form/json/{app}/{name}

**Submit a contact form as JSON**

`operationId: CrmFormController_saveContactFormJSON`

Public. The first post for a new `name` creates the `crm_form` (its schema generated from the body's shape) so later posts validate against it; each submission is stored in `form_submission`.

#### Signature

```http
POST /crm/contact-form/json/{app}/{name} (app: string, name: string, type?: string, body) -> The submission result
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | SAVE_FAILED | Error saving data: <reason> | The submission could not be stored. | — |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /crm/contact-form/post/{app}/{name}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `app` | path | string | yes | Application the form belongs to. |
| `name` | path | string | yes | Form name. |
| `type` | query | string | — |  |

### Request body

The submitted values.

```json
{
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "message": "When do you ship to Ireland?"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The submission result |
| `400` | Error saving data: <reason> — The submission could not be stored. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/form/{name}

**Open a form**

`operationId: CrmFormController_getCRMForm`

Public. What a visitor needs to draw and send a form — never the internal record. The reply carries `form` (name, title, description, submitMessage, authenticationType, accessMode, status, dates, schema, pages), `collection` for a collection-bound form, the `schema` to render, `authentication` (`{ required, type, satisfied }`), `participant`, `submitter`, `session` (the pending submission a link/code opened, with any saved `values`), `displayName` and `logo`.

Gates, in order: the form (or a collection shared under that name) must exist; an explicit `read` list must include `Guest` (a form with no `read` list is open; a collection never is unless shared); the form must be open (status and start/end dates); `accessMode` `code` needs the form's access code and `participants` a participant's code; and `authenticationType` `magic-link` / `code` needs the token from the email, `email` asks for an address, `password` a signed-in customer. Refusals carry `{ message, reason, title }` so the page can ask for the right thing.

#### Signature

```http
GET /crm/form/{name} (name: string, code?: string, email?: string, token?: string) -> { form, collection, schema, authenticationType, authentication, participant, submitter, session, displayName, logo }
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | FORM_NOT_FOUND | No form or collection found with name <name> | No crm_form and no collection by that name (a form bound to a missing collection: "Collection not found"). | — |
| `403` | NOT_PUBLIC | This form is not available for public access | An explicit `read` list leaves out Guest (a collection: "This collection is not shared publicly"). | — |
| `401` | CODE_REQUIRED | This form needs an access code. | accessMode `code` with no code, or `participants` with no code ("This form is by invitation. Open it from the link you were sent."). Body `reason: "code-required"`. | — |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /crm/form/submit/{name}/{email}`
- `POST /crm/form/{name}/access`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Form name (or a publicly shared collection name). |
| `code` | query | string | — | Access code or participant code. |
| `x-form-code` | header | string | — | Access code (alternative to `code`). |
| `email` | query | string | — | For forms whose authenticationType is `email`. |
| `token` | query | string | — | Token from a magic link or one-time code email. |
| `x-form-token` | header | string | — | Link token (alternative to `token`). |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { form, collection, schema, authenticationType, authentication, participant, submitter, session, displayName, logo } |
| `401` | This form needs an access code. — accessMode `code` with no code, or `participants` with no code ("This form is by invitation. Open it from the link you were sent."). Body `reason: "code-required"`. |
| `403` | This form is not available for public access — An explicit `read` list leaves out Guest (a collection: "This collection is not shared publicly"). |
| `404` | No form or collection found with name <name> — No crm_form and no collection by that name (a form bound to a missing collection: "Collection not found"). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/form/{name}/access

**Ask for a form link or code**

`operationId: CrmFormController_requestFormAccess`

Public. For a form whose authenticationType is `magic-link` or `code`: the person gives their email, is added to the participants, and the invitation with their link and code is emailed to them. Nothing about the form is revealed by asking.

#### Signature

```http
POST /crm/form/{name}/access (name: string, body) -> { sent: true, email, via, session }
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EMAIL_REQUIRED | A valid email address is required | `email` missing or has no @. | — |
| `404` | FORM_NOT_FOUND | No form named <name> | No crm_form with that name. | — |
| `403` | FORM_NOT_OPEN | This form is not open yet. | The form is closed, ended, or outside its dates (body `reason`: not-open-yet \| closed). | — |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Form name. |

### Request body

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { sent: true, email, via, session } |
| `400` | A valid email address is required — `email` missing or has no @. |
| `403` | This form is not open yet. — The form is closed, ended, or outside its dates (body `reason`: not-open-yet \| closed). |
| `404` | No form named <name> — No crm_form with that name. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/form/submit/{name}/{email}

**Submit a form**

`operationId: CrmFormController_submitCRMForm`

Public. Submits a completed form. The same gates as `GET /crm/form/{name}` apply first; then the identity the form asks for is checked for submitting, an explicit `create` list must include `Guest` (a collection must always grant it), and the body is validated against the schema — missing required fields come back as "Please fill in <field titles>." The access code may be sent as `accessCode` in the body, `x-form-code` or `?code=`; a link token as `token`, `x-form-token` or `?token=`.

#### Signature

```http
POST /crm/form/submit/{name}/{email} (name: string, email: string, body) -> The saved submission (form_submission, or a record of the bound collection)
```

#### Access

Public — no credentials required.

#### Notes

- Notification email to the form's `data.emails` is billed. An org without credit still stores the submission but sends nothing, so do not rely on the email as your only signal.
- Guard the client against double submission. Disabling the button is not enough; a second submit event produces a second record.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | FORM_DATA_REQUIRED | Form data is required | Empty body. | — |
| `404` | FORM_NOT_FOUND | No form or collection found with name <name> | No crm_form and no collection by that name (a form bound to a missing collection: "Collection not found"). | — |
| `403` | NOT_PUBLIC | This form is not available for public access | An explicit `read` list leaves out Guest (a collection: "This collection is not shared publicly"). | — |
| `401` | CODE_REQUIRED | This form needs an access code. | accessMode `code` with no code, or `participants` with no code ("This form is by invitation. Open it from the link you were sent."). Body `reason: "code-required"`. | — |

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /crm/form/{name}`
- `POST /client/forms/{name}/submit`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Form name. |
| `email` | path | string | yes | Submitter email, when known (also read from `body.email`). |

### Request body

The submitted values, as a flat object keyed by the schema's field names.

```json
{
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "message": "When do you ship to Ireland?"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The saved submission (form_submission, or a record of the bound collection) |
| `400` | Form data is required — Empty body. |
| `401` | This form needs an access code. — accessMode `code` with no code, or `participants` with no code ("This form is by invitation. Open it from the link you were sent."). Body `reason: "code-required"`. |
| `403` | This form is not available for public access — An explicit `read` list leaves out Guest (a collection: "This collection is not shared publicly"). |
| `404` | No form or collection found with name <name> — No crm_form and no collection by that name (a form bound to a missing collection: "Collection not found"). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/form/{name}/invite

**Invite a form's participants**

`operationId: CrmFormController_inviteParticipants`

Staff only. Emails each participant their own link and access code (a code is minted for anyone without one). Pass `emails` to send to those participants only; omit it for everyone. Safe to repeat: a resend is marked as a reminder and never changes a code, so a link already sent keeps working. Uses the form's `invitationTemplate`, else `form-invitation`. A `new` form becomes `sent`.

#### Signature

```http
POST /crm/form/{name}/invite (name: string, body) -> { form, template, sent, failed, results: [{ email, sent, reminder, inviteCount, link }], participants }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `User`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | FORM_NOT_FOUND | No form named <name> | No crm_form with that name. | — |
| `409` | NO_PARTICIPANTS | This form has no participants to invite. | The form lists no participants. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Form name. |

### Request body

```json
{
  "emails": [
    "ada@example.com"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { form, template, sent, failed, results: [{ email, sent, reminder, inviteCount, link }], participants } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No form named <name> — No crm_form with that name. |
| `409` | This form has no participants to invite. — The form lists no participants. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/forms/overview

**Forms overview**

`operationId: CrmFormController_getFormsOverview`

Staff only. Everything the Forms dashboard shows: totals (forms, public, open, collection-bound, on a workflow, in a workflow now, started-not-finished, submissions, submissions in the period, awaiting review), counts by status, a row per form with its own numbers, and the most recent submissions. Counts cover inline forms (`form_submission`) and collection-bound forms (records of the bound collection).

#### Signature

```http
GET /crm/forms/overview (period?: string) -> { period, since, totals, byStatus, forms, recent }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `User`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `period` | query | "today" \| "week" \| "month" \| "year" | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { period, since, totals, byStatus, forms, recent } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/forms/submissions

**All form submissions**

`operationId: CrmFormController_listAllSubmissions`

Staff only. Submissions across every form, newest first, paged. Each row carries the form it answers and, when it is on a workflow, `workflowInfo` (workflow, named stage, task, stage history).

#### Signature

```http
GET /crm/forms/submissions (formId?: string, status?: string, email?: string, inWorkflow?: string, page?: integer, pageSize?: integer) -> { data, total, page, pageSize }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `User`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `formId` | query | string | — |  |
| `status` | query | string | — |  |
| `email` | query | string | — |  |
| `inWorkflow` | query | "true" | — |  |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { data, total, page, pageSize } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/collection-form/{number}/{demo}-email?

**Send a form to its participants (legacy)**

`operationId: CrmFormController_sendCRMForm`

Staff. Kept for older clients: takes the form name from `body.name` (or `body.form.data.name`) and does exactly what `POST /crm/form/{name}/invite` does, including `body.emails`. The path segments are ignored. The route is declared as `collection-form/:number/:demo-email?`, which Express reads as a parameter `demo` followed by the literal text `-email?` — prefer the invite route.

#### Signature

```http
POST /crm/collection-form/{number}/{demo}-email? (number: string, demo: string, body) -> Same as the invite route
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | NAME_REQUIRED | form name is required | No form name in the body. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/form/{name}/invite`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `number` | path | string | yes | Ignored. |
| `demo-email` | path | string | yes |  |
| `demo` | path | string | yes | Ignored. |

### Request body

```json
{
  "name": "grant-application"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Same as the invite route |
| `400` | form name is required — No form name in the body. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /crm/form/{id}

**Delete a form**

`operationId: CrmFormController_deleteForm`

Staff only. Deletes the form definition (`crm_form`). Its submissions are not deleted.

#### Signature

```http
DELETE /crm/form/{id} (id: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `User`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

