# ClickUp Support Integration

The support form on the dealer dashboard submits tickets directly to a ClickUp list via a single endpoint. This doc covers the request shape, configuration, and failure modes.

## Purpose

`POST /api/support/clickup` - creates a new ClickUp task in a configured list from a support-form submission, with an optional file attachment.

- **Creates** one task per request.
- **Does not** update existing tasks, add comments to tasks, or sync status.
- **Does not** queue or retry - requests that fail to reach ClickUp return an error to the client (see [Error Handling](#error-handling)).

Source: `app/api/support/clickup/route.ts`.

## Request Shape

The endpoint accepts `multipart/form-data` only. JSON bodies are not handled.

### Form Fields

| Field          | Required              | Max          | Notes                                                                                                                   |
| -------------- | --------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `subject`      | yes                   | 255 chars    | Becomes the ClickUp task `name`                                                                                         |
| `description`  | yes                   | 10,000 chars | Becomes the ClickUp task `description`                                                                                  |
| `attachment`   | no                    | 5 MB         | Accepted types: `image/jpeg`, `image/png`, `image/webp`, `application/pdf`                                              |
| `email`        | no                    | -            | Written to a ClickUp custom field if `CLICKUP_EMAIL_FIELD_ID` is configured                                             |
| `dealerName`   | no                    | -            | Appended to the task description as context                                                                             |
| `dealerNumber` | no                    | -            | Appended to the task description as context                                                                             |
| `website`      | no                    | -            | Honeypot - a filled value causes the request to silently succeed without creating a task (`route.ts:98-103`)            |
| `_ts`          | no                    | -            | Milliseconds since form render; submissions under 3000 ms silently succeed without creating a task (`route.ts:105-114`) |
| `_turnstile`   | yes (when configured) | -            | Cloudflare Turnstile token; verified via `verifyTurnstile` before any ClickUp call (`route.ts:117-123`)                 |

Validation rules are defined in `validatePayload` (`route.ts:32-54`) and `validateFile` (`route.ts:56-70`).

### Field Mapping to ClickUp

The outgoing ClickUp task payload (`route.ts:149-171`):

```jsonc
{
  "name": "<subject>",
  "description": "<description>\n\n---\nDealer Name: <dealerName>\nDealer Number: <dealerNumber>",
  "assignees": [<CLICKUP_ASSIGNEE_ID>],        // only if env var set and parses to an int
  "custom_fields": [
    { "id": "<CLICKUP_EMAIL_FIELD_ID>", "value": "<email>" }  // only if both set
  ]
}
```

Dealer name/number are concatenated into the description with a `---` separator (`route.ts:138-147`); they are not sent as structured fields.

If an `attachment` is present, a second request uploads it to `POST /api/v2/task/{taskId}/attachment` after the task is created (`route.ts:204-238`).

## ClickUp Workspace / List Target

The endpoint is config-driven. All target IDs come from environment variables - no list or workspace IDs are hardcoded.

| Env var                  | Purpose                                                                                               | Required |
| ------------------------ | ----------------------------------------------------------------------------------------------------- | -------- |
| `CLICKUP_API_TOKEN`      | Personal API token used for `Authorization` header                                                    | yes      |
| `CLICKUP_LIST_ID`        | Destination list - the endpoint posts to `https://api.clickup.com/api/v2/list/{CLICKUP_LIST_ID}/task` | yes      |
| `CLICKUP_ASSIGNEE_ID`    | Numeric ClickUp user ID auto-assigned on every task                                                   | no       |
| `CLICKUP_EMAIL_FIELD_ID` | Custom-field ID that receives the submitter's email                                                   | no       |

Workspace and space are implicit - the list ID is globally unique within a ClickUp workspace, so the workspace is determined by which token + list pair is configured. Confirm the list lives in the correct workspace in the ClickUp UI before rotating values.

If either `CLICKUP_API_TOKEN` or `CLICKUP_LIST_ID` is missing, the endpoint logs an error and returns `503` immediately (`route.ts:77-83`).

## Auth

Outbound auth to ClickUp is a bearer-style token passed in the `Authorization` header (`route.ts:178-180`):

```
Authorization: <CLICKUP_API_TOKEN>
```

ClickUp personal tokens begin with `pk_`. The token is used for both task creation and attachment upload. There is no token refresh - rotate manually via the ClickUp UI when needed and redeploy with the new value.

Inbound auth: none. The endpoint is public but protected by three layers of spam controls:

1. **Honeypot** (`website` field) - bot submissions silently succeed.
2. **Minimum submit time** (3000 ms) - too-fast submissions silently succeed.
3. **Cloudflare Turnstile** - server-side token verification via `verifyTurnstile` from `lib/turnstile.ts`.

See `docs/TURNSTILE_SETUP.md` for Turnstile configuration.

## Error Handling

| Condition                                                  | Response                      | Side effect                                                                       |
| ---------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------- |
| Missing `CLICKUP_API_TOKEN` or `CLICKUP_LIST_ID`           | `503` with generic error      | Logged as `error`                                                                 |
| Honeypot filled                                            | `200 {success: true}`         | No task created; logged as `info`                                                 |
| Submitted under 3000 ms                                    | `200 {success: true}`         | No task created; logged as `info`                                                 |
| Turnstile verification fails                               | `400` with Turnstile error    | No task created                                                                   |
| Validation failure (missing/oversized subject/description) | `400` with specific message   | -                                                                                 |
| Invalid file (too large, wrong type)                       | `400` with specific message   | -                                                                                 |
| ClickUp task-creation fetch is non-OK                      | `502` with generic error      | Logged with status and response body                                              |
| Uncaught exception                                         | `500` with generic error      | Logged with error object                                                          |
| Attachment upload fails after task creation                | `200 {success: true, taskId}` | Logged as `warn`; **task is created without the attachment** (`route.ts:228-237`) |

### Queuing and Retries

**Tickets are not queued or retried.** A transient ClickUp outage during task creation surfaces as a `502` to the client, and the user must resubmit.

The attachment-upload branch is the exception: if task creation succeeds but attachment upload fails, the task is kept and the failure is logged rather than surfaced to the client. The user sees a success response and has no indication the file did not attach. **Verify this behavior before changing it** - this matches the current code but may be a product decision worth revisiting.

## Troubleshooting

### "Ticket didn't appear in ClickUp"

Work through these in order:

1. **Check server logs for the request.** Look for `ClickUp task created` (success), `ClickUp API error: failed to create task` (ClickUp rejected the request), `Spam detected` (honeypot or timing check intercepted), or `ClickUp API not configured` (env vars missing).
2. **Confirm env vars are set on the deployed environment.** `CLICKUP_API_TOKEN` and `CLICKUP_LIST_ID` are required. Dev, staging, and prod have independent values.
3. **Verify the list ID points at the correct list in the correct workspace.** A list ID from the wrong workspace will fail with a ClickUp `401`/`403`.
4. **Check the token has write access to the list.** Personal tokens inherit the user's permissions - if the user doesn't see the list in their ClickUp UI, the token can't write to it either.
5. **Confirm Turnstile isn't silently blocking the submission.** A failed Turnstile check returns `400` to the client. `verifyTurnstile` logs the failure reason; see `docs/TURNSTILE_SETUP.md`.
6. **Rule out spam protection.** If the form was submitted in under 3 seconds or the hidden `website` field had a value, the endpoint returns success without creating a task. Reload the form and submit again.

### "Ticket appeared but the attachment is missing"

Check logs for `ClickUp API warning: failed to upload attachment`. The task is created before the attachment is uploaded; attachment failures are non-fatal (`route.ts:228-237`). Common causes: file corruption during `formData` parsing, ClickUp attachment API rate limiting, or network interruption between the two API calls.

### "Assignee isn't being set"

`CLICKUP_ASSIGNEE_ID` must parse as an integer (`route.ts:156-161`). String IDs, emails, or usernames will silently be ignored. Find the numeric ID in the ClickUp API (`GET /team`) or by inspecting a task payload in the network tab of the ClickUp UI.

### "Email custom field is blank"

Both `CLICKUP_EMAIL_FIELD_ID` must be set **and** the form submission must include an `email` value (`route.ts:164-171`). The dashboard passes `userEmail` from the session; unauthenticated submissions from the public support form require the user to fill the email field.
