> ## Documentation Index
> Fetch the complete documentation index at: https://heybtw.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# record_signups

> Record who registered for and who attended one of your collabs, from a guest export or rows you hold.

> [!WARNING]
>
> Writes to your heyBTW workspace on `action: "confirm"`: sign-ups and their statuses on one collab. A preview or append writes nothing to the roster. It sends nothing, enriches no one, never writes an invited record, and never writes to the event platform.

Reach for it when the user has a guest list or check-in export from a platform heyBTW does not sync (Partiful, Eventbrite, a badge scan, a spreadsheet). For a file with more than about 50 rows or custom question columns, prefer [create\_import\_link](https://heybtw.com/docs/tools/create-import-link.md).

Follows the [heyBTW MCP rules](https://heybtw.com/docs/mcp/rules.md).

## When to use it

* "Here's the Partiful export from Thursday. Load it."
* "These 12 people checked in at the door, here are their LinkedIn links."
* "Mark everyone on this badge scan as attended."
* "Our HubSpot list is everyone who registered for the March dinner."

## Parameters

| Parameter | Type | Required | Notes |
| - | - | - | - |
| `channel_id` or `event_name` | string | One of them | The collab. An ambiguous name records nothing and lists the candidates. |
| `action` | string | No | `preview` (default), `append` or `confirm`. |
| `csv` | string | One of `csv` / `rows` on preview | The export file's text UNCHANGED, header first, at most 500 data rows. Never reformat, rename columns or write a mapping script. |
| `rows` | object\[] | One of `csv` / `rows` on preview; required on append | At most 500. Each needs `attendance_status` and `email` (or `linkedin_url` or `name`, below). Optional: `registered_at` (RFC 3339 with an offset), `name`, `company`, `title`, `linkedin_url`, `registration_answers`. Any other key is named in `unconsumed_fields`. |
| `preview_token` | string | On append and confirm | From the preview. |
| `client_request_id` | string | On append and confirm | A fresh hyphenated UUID per call. |
| `default_status` | string | No | Status for csv rows with no status. Without it those rows are skipped and `needs` asks for it. Ask the user, never guess. |
| `column_map` | object | No | `{"<column>": "<target>"}`: a field name, `registration_question` or `ignored`. Preview again to change a column. |
| `match_by` | string | No | `email`, `linkedin_url` or `name` for every row. Without it each row uses email, else linkedin\_url, else name. |
| `allow_status_correction` | boolean | No | Only when the user confirms a lower or different status is right. |
| `exclude_suspect_kinds` | string\[] | No | Any of `spreadsheet_error`, `notetaker_bot`, `resource_address`, `role_address`, `plus_addressed`. Only on the user's say. |
| `source` | string | No | Provenance: `partiful`, `luma`, `eventbrite`, `splash`, `zuddl`, `spreadsheet`, `other`, or `crm_list`. Never send `crm_list` for a platform export. |
| `source_ref` | object | With `crm_list` | `{crm, object, id, name, member_created_range}`. |
| `contract_version` | string | No | Omit for v1, the only version advertised. |

`attendance_status` on `rows` must be exactly one of `CHECKED_IN`, `APPROVED`, `PENDING_APPROVAL`, `DECLINED`, `WAITLIST`, `REGISTERED`.

## Status words

A `csv` is read with heyBTW's one status vocabulary, the one the dashboard importer reads. Confirmed, Approved, Attended, Can't Go, Cancelled, No response and Invited were checked on a live preview today; Attending and Going are from the tool description.

| Word in the file | Recorded as | Layer |
| - | - | - |
| Confirmed, Attending, Approved, Going | `APPROVED` | registered |
| Attended | `CHECKED_IN` | attended |
| Can't Go, Cancelled | `DECLINED` | declined |
| No response, Invited | skipped, `invited_not_accepted` | none |

Registered is never attended. Only `CHECKED_IN` is attended. INVITED is never recorded here: people the user wants to reach go through import\_people, save\_list, activate\_list. A word heyBTW does not know is skipped and listed with the word; ask the user once what it means.

## Workflow

**1. Preview**

`action: "preview"` with the `csv` or the first `rows`. Nothing is written. The server keeps the rows and returns `preview_token`, valid 15 minutes.

**2. Append (rows only, more than about 50)**

`action: "append"` with the same `preview_token`, the next chunk of `rows` and a fresh `client_request_id`. Never a csv. The token stays valid 15 minutes after the last append. Report `data[0].staged` totals, never one chunk's. At most 2,000 rows staged.

**3. Show the preview to the user**

The `column_map`, the counts, the skipped rows, `matches` for LinkedIn or name rows, suspect rows, and the `workspace_overlap.sentence` and `company_carry.sentence` as written.

**4. Confirm**

`action: "confirm"`, the `preview_token` and a fresh `client_request_id`, with NO rows and NO csv. Never resend the rows. A second confirm with the same token replays the first result.

**5. Close the import gaps**

Read `import_gaps` and propose each remedy tool, manage\_collab\_partners first, before you report the import done.

On a transport error, retry the confirm with the SAME `client_request_id`: a committed call is replayed (`idempotent_replay: true`), never repeated. Reusing an id for different rows is refused. Expired or refused token: preview again.

### LinkedIn and name rows

A row with `linkedin_url` or `name` and no email matches the ONE sign-up already on the collab with that profile or full name (case and accents folded). It only updates that person, never adds anyone. Never look the emails up first.

| Reason | Meaning | What to do |
| - | - | - |
| matched | `data[0].matches` names the person (name and email) | Show them to the user before confirming. |
| `no_match_on_collab` | No one on this collab has that link or name. Nothing is created. | Check the link or name, or send the person's email to add them. |
| `ambiguous_linkedin`, `ambiguous_name` | Two or more people share it, or it is a company page. | Ask the user for that person's email. |
| `match_changed` | The roster changed between preview and confirm. | Preview again. |

These rows confirm ONLY by `preview_token`.

### The event-history ladder

An export is rung 3: a snapshot. Rung 1, API sync, keeps partners, check-ins and answers current. A collab whose registrations Luma, Splash or Zuddl sync is refused here, naming the platform. Every answer carries `import_gaps`:

| gap | remedy\_tool |
| - | - |
| `partners` | manage\_collab\_partners |
| `attendance` | record\_signups |
| `registration_answers` | record\_signups |

A CRM list (rung 4, `source: "crm_list"`) says who was on it, never who came. Ask the user which cluster of create dates is this event, and whether the list means invited, registered or attended, before any call.

## Response

A preview `data[0]`, in the order that matters:

| Field | Meaning |
| - | - |
| `written` | false on preview and append. |
| `counts` | create, update, unchanged, rejected, failed. |
| `row_outcomes` | One per row you sent, in order. |
| `attention` | Every rejected row and every status that stays, with `row`, `line` (csv), `reason`. |
| `layers` | registered, declined, attended. |
| `status_mix`, `status_words` | By final status, and by the file's own words. |
| `column_map` | Each column: `maps_to`, `disposition`, `why`, `filled_rows`. |
| `matches` | LinkedIn and name rows only. |
| `suspect_count`, `suspect_by_kind` | Rows that may not be a person. Null when not classified. |
| `workspace_overlap.sentence`, `company_carry.sentence` | Tell the user as written. |
| `import_gaps` | The ladder gaps. |
| `first_value_preview` | registered and no\_company\_yet now; `served_on_confirm` lists the facts the confirm adds. |
| `preview_token`, `expires_at` | 15 minutes. |

A confirm opens with `facts`, `carry` and `next` (as create\_collab serves them), then `written: true`, `changed_since_preview` (rows whose result changed, each with `previewed` in `attention`) and `chunks`.

Trimmed from a real preview today (fictional people at example.org):

```json theme={null}
{
  "tool": "record_signups",
  "data": [
    {
      "action": "preview",
      "written": false,
      "counts": {"create": 3, "update": 0, "unchanged": 0, "rejected": 2, "failed": 0},
      "rejected_by_reason": {"invited_not_accepted": 1, "missing_email": 1},
      "attention": [
        {"row": 4, "person": "pat.invite@example.org", "outcome": "rejected", "reason": "invited_not_accepted", "line": 5},
        {"row": 5, "outcome": "rejected", "reason": "missing_email", "line": 6}
      ],
      "layers": {"registered": 2, "declined": 1, "attended": 1},
      "status_words": {
        "Approved": {"status": "APPROVED", "rows": 1},
        "Attended": {"status": "CHECKED_IN", "rows": 1},
        "Can't Go": {"status": "DECLINED", "rows": 1}
      },
      "preview_token": "unfM2B7PPzIqmkWj7o5txwFjNn6JfaUkXyH_5H_QYUA",
      "expires_at": "2026-11-03T19:52:09.376Z",
      "workspace_overlap": {"sentence": "None of these 3 people are in this workspace yet, and no one here has the same name under another email."},
      "company_carry": {"sentence": "3 of 3 rows carry company, title or LinkedIn."},
      "import_gaps": [
        {"gap": "partners", "remedy_tool": "manage_collab_partners"},
        {"gap": "attendance", "remedy_tool": "record_signups"},
        {"gap": "registration_answers", "remedy_tool": "record_signups"}
      ],
      "first_value_preview": {"registered": 2, "no_company_yet": 0, "via": "heybtw", "served_on_confirm": ["seen_before", "target_accounts", "known_by", "gap_to_target"]}
    }
  ],
  "status": "partial"
}
```

`status` is `partial` because rows were skipped (gap `partial_coverage`, retryable false). That is a disclosure, not a failure.

## Rules the agent must follow

* Send the file as it is. Never recount it yourself: relay the first coverage note's counts.
* Never confirm before every chunk is appended. Never resend rows on confirm.
* Never guess a status or a default status. Never mark a whole guest list attended.
* Never exclude a suspect kind on your own, and never call two emails the same person without the user's confirmation.
* A stale export never lowers a status. A fuller one advances it. Use `allow_status_correction` only on the user's say.

## Errors and gaps

| Code | What to do |
| - | - |
| `invalid_parameter` | Fix the named parameter. With no collab named, it asks for `channel_id` or `event_name`. |
| `entity_not_found` | Unknown, another workspace's or the demo collab. Create one with create\_collab. |
| `import_in_progress` | One import per collab at a time. Retry later with the same `client_request_id`. |
| `preview_token_expired`, `preview_token_used` and other token refusals | Preview again. |
| `insufficient_scope` | The OAuth token lacks `mcp:tools:write` (needed for every action). |
| 429 | Honor `Retry-After`. 30 requests per 5 s, 60 per minute, 1000 per day per workspace. |

## Related

* [create\_import\_link](https://heybtw.com/docs/tools/create-import-link.md), for a large file
* [create\_collab](https://heybtw.com/docs/tools/create-collab.md)
* [get\_event\_details](https://heybtw.com/docs/tools/get-event-details.md), to read the roster back
* Playbook: [Record who registered and who came](https://heybtw.com/docs/playbooks/record-who-came.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.