twenty-app-intake 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francisco Contreras
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ <div align="center">
2
+ <img src="./public/logo.svg" width="80" height="80" alt="Intake logo" />
3
+ <h1>Intake</h1>
4
+ <p><strong>The missing ingestion layer for Twenty CRM.</strong><br/>
5
+ Wire any form, webhook, or data source to Twenty. Leads land clean — every time.</p>
6
+
7
+ [![npm version](https://img.shields.io/npm/v/twenty-app-intake?color=0F172A&labelColor=334155&label=npm)](https://www.npmjs.com/package/twenty-app-intake)
8
+ [![Twenty](https://img.shields.io/badge/Twenty-%3E%3D2.5.0-0F172A?labelColor=334155)](https://twenty.com)
9
+ [![Tests](https://img.shields.io/badge/tests-68%20passing-22c55e?labelColor=334155)](https://github.com/FranciscoContreras/twenty-app-intake/actions)
10
+ [![License](https://img.shields.io/badge/license-MIT-0F172A?labelColor=334155)](./LICENSE)
11
+ [![Website](https://img.shields.io/badge/website-wearemachina.com-0F172A?labelColor=334155)](https://wearemachina.com)
12
+
13
+ <sub>Built by <a href="https://wearemachina.com"><strong>Machina</strong></a></sub>
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## The problem
19
+
20
+ Your contact forms, pipeline scrapers, and partner APIs each have their own field names. `phone_number` here, `phoneNumber` there, `tel` somewhere else. Half the time a new field appears and breaks your Zap. The other half, someone enters a duplicate that your team has to clean manually.
21
+
22
+ Intake handles all of it automatically.
23
+
24
+ ## What it does
25
+
26
+ Send any JSON payload to Intake's webhook. It figures out the rest.
27
+
28
+ ```bash
29
+ curl -X POST https://your-crm.com/s/intake/getting-started \
30
+ -H "Content-Type: application/json" \
31
+ -d '{
32
+ "first_name": "Jane",
33
+ "last_name": "Doe",
34
+ "email": "jane@acme.com",
35
+ "phone_number": "415-555-0199",
36
+ "company": "Acme Inc",
37
+ "message": "Need a new website by Q3.",
38
+ "utm_source": "google",
39
+ "budget": "25000"
40
+ }'
41
+ ```
42
+
43
+ **What Twenty gets:**
44
+ - ✅ Person record — Jane Doe, jane@acme.com, +1 415 555 0199
45
+ - ✅ Company record — Acme Inc (linked to Jane)
46
+ - ✅ Opportunity — "Getting Started — Jane Doe", stage: NEW
47
+ - ✅ Note — message + UTM source, formatted and attached
48
+ - ✅ Custom field `extBudget` auto-created on Person (first time only)
49
+
50
+ No Zaps. No middleware. No broken automations when your form adds a field.
51
+
52
+ ---
53
+
54
+ ## How it works
55
+
56
+ ```
57
+ Any JSON payload
58
+
59
+
60
+ ① Normalize phone_number → phone, emailAddress → email, firstName + lastName → name
61
+
62
+
63
+ ② Classify short values → CRM fields │ prose / UTMs → note
64
+
65
+
66
+ ③ Extend unknown fields → auto-create ext_ custom fields on Person or Company
67
+
68
+
69
+ ④ Deduplicate match by email (Person) or domain (Company) before creating anything
70
+
71
+
72
+ ⑤ Ingest Person + Company + Opportunity + Note — one webhook, the full chain
73
+
74
+
75
+ ⑥ Log every ingestion recorded in IntakeLog with status, timing, field counts
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Get started
81
+
82
+ ### 1. Install
83
+
84
+ From the Twenty marketplace in **Settings → Applications**, search for **Intake** and install.
85
+
86
+ On fresh install, Intake automatically creates a "Getting Started" source with a ready-to-use webhook URL.
87
+
88
+ ### 2. Register a source
89
+
90
+ ```bash
91
+ curl -X POST https://your-crm.com/s/intake/sources/register \
92
+ -H "Authorization: Bearer YOUR_API_KEY" \
93
+ -H "Content-Type: application/json" \
94
+ -d '{
95
+ "name": "Contact Form",
96
+ "slug": "contact-form",
97
+ "targetObject": "AUTO"
98
+ }'
99
+ ```
100
+
101
+ ```json
102
+ {
103
+ "webhookUrl": "https://your-crm.com/s/intake/contact-form",
104
+ "secret": "wh_live_abc123..."
105
+ }
106
+ ```
107
+
108
+ ### 3. Test without writing anything
109
+
110
+ ```bash
111
+ curl -X POST https://your-crm.com/s/intake/contact-form/test \
112
+ -H "Content-Type: application/json" \
113
+ -d '{"first_name":"Jane","email":"jane@co.com","budget":"15000"}'
114
+ ```
115
+
116
+ Returns exactly what *would* be created — standard fields, custom fields to create, note preview — without touching the CRM.
117
+
118
+ ---
119
+
120
+ ## Payload formats
121
+
122
+ Intake accepts any valid JSON. No required fields.
123
+
124
+ **Flat (contact form):**
125
+ ```json
126
+ {
127
+ "first_name": "Jane",
128
+ "email": "jane@acme.com",
129
+ "company": "Acme",
130
+ "message": "Looking for a full rebrand.",
131
+ "utm_source": "google"
132
+ }
133
+ ```
134
+
135
+ **Structured (pipeline app):**
136
+ ```json
137
+ {
138
+ "company": {
139
+ "name": "Acme Plumbing",
140
+ "domainName": { "primaryLinkUrl": "https://acmeplumbing.com" },
141
+ "address": { "addressCity": "San Jose", "addressState": "CA" }
142
+ },
143
+ "person": {
144
+ "name": { "firstName": "John", "lastName": "Smith" },
145
+ "emails": { "primaryEmail": "john@acmeplumbing.com" }
146
+ },
147
+ "google_rating": 4.7,
148
+ "review_count": 143,
149
+ "analysis": "Strong reviews, outdated website."
150
+ }
151
+ ```
152
+
153
+ **Arbitrary nested:**
154
+ ```json
155
+ {
156
+ "submitted_by": { "full_name": "Alex Thompson", "contact_email": "alex@co.com" },
157
+ "project": { "type": "SaaS Dashboard", "budget": "15k" },
158
+ "referrer": "behance"
159
+ }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Built-in field normalization
165
+
166
+ 100+ mappings ship by default. Some highlights:
167
+
168
+ | Incoming key | Twenty field |
169
+ |---|---|
170
+ | `phone`, `phone_number`, `phoneNumber`, `tel`, `mobile`, `cell` | `phones.primaryPhoneNumber` |
171
+ | `email`, `email_address`, `contact_email` | `emails.primaryEmail` |
172
+ | `first_name`, `firstName`, `fname` | `name.firstName` |
173
+ | `last_name`, `lastName`, `surname` | `name.lastName` |
174
+ | `name`, `full_name`, `fullName` | `name` (auto-split) |
175
+ | `company`, `company_name`, `business`, `organization` | Company record |
176
+ | `website`, `url`, `domain`, `homepage` | `domainName.primaryLinkUrl` |
177
+ | `utm_source/medium/campaign/content/term` | Note (always) |
178
+ | `message`, `description`, `notes`, `comments`, `analysis` | Note (always) |
179
+
180
+ Unknown fields get an `ext_` prefix and are created as custom fields the first time they appear.
181
+
182
+ ---
183
+
184
+ ## Custom field rules
185
+
186
+ Add `IntakeFieldRule` records to extend or override the built-in map for a specific source or globally:
187
+
188
+ | Field | Description |
189
+ |---|---|
190
+ | `inputPattern` | Exact key name or JavaScript regex |
191
+ | `canonicalName` | Target field in Twenty (use `ext` prefix for custom fields) |
192
+ | `fieldType` | `TEXT`, `NUMBER`, `LINKS`, `EMAILS`, `PHONES`, `BOOLEAN`, `DATE_TIME`, `NOTE`, or `SKIP` |
193
+ | `priority` | Higher = checked first (0–100) |
194
+
195
+ Rules with no source linked apply globally across all sources.
196
+
197
+ ---
198
+
199
+ ## Source configuration
200
+
201
+ Each `IntakeSource` record controls:
202
+
203
+ | Field | Default | Description |
204
+ |---|---|---|
205
+ | `targetObject` | `AUTO` | `PERSON`, `COMPANY`, or auto-detect |
206
+ | `webhookSecret` | — | HMAC-SHA256 signing secret |
207
+ | `createOpportunity` | `true` | Auto-create Opportunity per ingestion |
208
+ | `opportunityNameTemplate` | `{{source}} — {{firstName}} {{lastName}}` | Supports `{{source}}`, `{{firstName}}`, `{{lastName}}`, `{{email}}`, `{{company}}` |
209
+ | `status` | `ACTIVE` | Pause a source without deleting it |
210
+
211
+ ---
212
+
213
+ ## Workspace settings
214
+
215
+ Configurable from **Settings → Applications → Intake → Custom**:
216
+
217
+ | Setting | Default | Description |
218
+ |---|---|---|
219
+ | `INTAKE_APP_LABEL` | `Intake` | Name used in note titles and opportunity names |
220
+ | `INTAKE_DEFAULT_OPP_STAGE` | `NEW` | Stage for auto-created Opportunities |
221
+ | `INTAKE_FIELD_CREATION_ENABLED` | `true` | Toggle auto-schema extension |
222
+ | `INTAKE_MAX_EXT_FIELDS` | `50` | Cap on custom fields per object |
223
+ | `INTAKE_DEDUP_WINDOW_MINUTES` | `5` | Duplicate suppression window |
224
+ | `INTAKE_REQUIRE_HMAC` | `false` | Enforce signed webhooks globally |
225
+
226
+ ---
227
+
228
+ ## Webhook security
229
+
230
+ Sign requests with `HMAC-SHA256` using the source's secret:
231
+
232
+ ```bash
233
+ SECRET="your-signing-secret"
234
+ PAYLOAD='{"email":"jane@co.com"}'
235
+ SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
236
+
237
+ curl -X POST https://your-crm.com/s/intake/contact-form \
238
+ -H "Content-Type: application/json" \
239
+ -H "X-Webhook-Signature: sha256=$SIGNATURE" \
240
+ -d "$PAYLOAD"
241
+ ```
242
+
243
+ Sources without a secret accept unsigned requests — useful for internal tools. Set `INTAKE_REQUIRE_HMAC=true` to enforce signatures globally.
244
+
245
+ ---
246
+
247
+ ## Endpoints
248
+
249
+ | Method | Path | Auth | Description |
250
+ |---|---|---|---|
251
+ | `POST` | `/s/intake/:slug` | HMAC or open | Ingest a payload |
252
+ | `POST` | `/s/intake/:slug/test` | None | Dry-run — preview without writing |
253
+ | `POST` | `/s/intake/logs/:logId/retry` | API key | Retry a failed ingestion |
254
+ | `GET` | `/s/intake/health` | None | Health check |
255
+ | `POST` | `/s/intake/sources/register` | API key | Register a new source |
256
+
257
+ ---
258
+
259
+ ## Retry failed ingestions
260
+
261
+ Every ingestion is logged in `IntakeLog`. Failed logs can be retried from the record's detail page in Twenty, or via API:
262
+
263
+ ```bash
264
+ curl -X POST https://your-crm.com/s/intake/logs/LOG_ID/retry \
265
+ -H "Authorization: Bearer YOUR_API_KEY"
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ git clone https://github.com/FranciscoContreras/twenty-app-intake
274
+ cd twenty-app-intake
275
+ yarn install
276
+
277
+ # Run unit tests
278
+ yarn test
279
+
280
+ # Connect to your Twenty instance
281
+ yarn twenty remote add --api-url https://your-crm.com --api-key YOUR_KEY --as production
282
+
283
+ # Sync in watch mode
284
+ yarn twenty dev
285
+
286
+ # One-shot sync
287
+ yarn twenty dev --once
288
+ ```
289
+
290
+ ---
291
+
292
+ ## vs. the alternatives
293
+
294
+ | | Intake | Zapier/Make | Hookdeck | Custom webhook |
295
+ |---|---|---|---|---|
296
+ | Zero config | ✅ | ❌ Manual mapping | ❌ Write ingestion logic | ❌ Build everything |
297
+ | Auto schema extension | ✅ | ❌ New fields break flows | ❌ | ❌ |
298
+ | Native Twenty objects | ✅ | ❌ | ❌ | ❌ |
299
+ | Deduplication | ✅ | Partial | ❌ | Roll your own |
300
+ | Audit log | ✅ | ❌ | ✅ | ❌ |
301
+ | Self-hosted | ✅ | ❌ | Paid | ✅ |
302
+ | Open source | ✅ MIT | ❌ | ❌ | ✅ |
303
+
304
+ ---
305
+
306
+ ## License
307
+
308
+ MIT — built by [Machina](https://wearemachina.com) · [FranciscoContreras](https://github.com/FranciscoContreras)
package/manifest.json CHANGED
@@ -47,7 +47,7 @@
47
47
  }
48
48
  },
49
49
  "yarnLockChecksum": "7dced8c0bfa9b803fa206e8556d7fb12",
50
- "packageJsonChecksum": "028e98795de1b242d2d465a5320d448c"
50
+ "packageJsonChecksum": "f3822ee45e822353dce91950b79f5c4e"
51
51
  },
52
52
  "objects": [
53
53
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twenty-app-intake",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The missing ingestion layer for Twenty CRM. Automatically sync any webhook source — contact forms, pipeline apps, or any platform — with native schema extension, field normalization, and deduplication. Zero manual configuration required.",
5
5
  "keywords": [
6
6
  "twenty-app",
@@ -18,6 +18,12 @@
18
18
  "automation"
19
19
  ],
20
20
  "license": "MIT",
21
+ "homepage": "https://wearemachina.com",
22
+ "author": "Machina (https://wearemachina.com)",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/FranciscoContreras/twenty-app-intake.git"
26
+ },
21
27
  "engines": {
22
28
  "node": ">=24.0.0",
23
29
  "twenty": ">=2.5.0"
@@ -30,7 +36,7 @@
30
36
  "test:coverage": "vitest run --config vitest.config.ts --coverage",
31
37
  "typecheck": "twenty typecheck",
32
38
  "build": "twenty build",
33
- "publish:app": "twenty publish"
39
+ "publish:app": "twenty build && cp README.md LICENSE .twenty/output/ && npm publish .twenty/output"
34
40
  },
35
41
  "dependencies": {
36
42
  "cockatiel": "^3.2.1",