npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

twenty-app-intake

v0.2.1

Published

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.

Downloads

252

Readme

npm version Twenty Tests License Website

Built by Machina


The problem

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.

Intake handles all of it automatically.

What it does

Send any JSON payload to Intake's webhook. It figures out the rest.

curl -X POST https://your-crm.com/s/intake/getting-started \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "last_name":  "Doe",
    "email":      "[email protected]",
    "phone_number": "415-555-0199",
    "company":    "Acme Inc",
    "message":    "Need a new website by Q3.",
    "utm_source": "google",
    "budget":     "25000"
  }'

What Twenty gets:

  • ✅ Person record — Jane Doe, [email protected], +1 415 555 0199
  • ✅ Company record — Acme Inc (linked to Jane)
  • ✅ Opportunity — "Getting Started — Jane Doe", stage: NEW
  • ✅ Note — message + UTM source, formatted and attached
  • ✅ Custom field extBudget auto-created on Person (first time only)

No Zaps. No middleware. No broken automations when your form adds a field.


How it works

Any JSON payload
      │
      ▼
① Normalize    phone_number → phone, emailAddress → email, firstName + lastName → name
      │
      ▼
② Classify     short values → CRM fields │ prose / UTMs → note
      │
      ▼
③ Extend       unknown fields → auto-create ext_ custom fields on Person or Company
      │
      ▼
④ Deduplicate  match by email (Person) or domain (Company) before creating anything
      │
      ▼
⑤ Ingest       Person + Company + Opportunity + Note — one webhook, the full chain
      │
      ▼
⑥ Log          every ingestion recorded in IntakeLog with status, timing, field counts

Get started

1. Install

From the Twenty marketplace in Settings → Applications, search for Intake and install.

On fresh install, Intake automatically creates a "Getting Started" source with a ready-to-use webhook URL.

2. Register a source

curl -X POST https://your-crm.com/s/intake/sources/register \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Contact Form",
    "slug": "contact-form",
    "targetObject": "AUTO"
  }'
{
  "webhookUrl": "https://your-crm.com/s/intake/contact-form",
  "secret": "wh_live_abc123..."
}

3. Test without writing anything

curl -X POST https://your-crm.com/s/intake/contact-form/test \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Jane","email":"[email protected]","budget":"15000"}'

Returns exactly what would be created — standard fields, custom fields to create, note preview — without touching the CRM.


Payload formats

Intake accepts any valid JSON. No required fields.

Flat (contact form):

{
  "first_name": "Jane",
  "email": "[email protected]",
  "company": "Acme",
  "message": "Looking for a full rebrand.",
  "utm_source": "google"
}

Structured (pipeline app):

{
  "company": {
    "name": "Acme Plumbing",
    "domainName": { "primaryLinkUrl": "https://acmeplumbing.com" },
    "address": { "addressCity": "San Jose", "addressState": "CA" }
  },
  "person": {
    "name": { "firstName": "John", "lastName": "Smith" },
    "emails": { "primaryEmail": "[email protected]" }
  },
  "google_rating": 4.7,
  "review_count": 143,
  "analysis": "Strong reviews, outdated website."
}

Arbitrary nested:

{
  "submitted_by": { "full_name": "Alex Thompson", "contact_email": "[email protected]" },
  "project": { "type": "SaaS Dashboard", "budget": "15k" },
  "referrer": "behance"
}

Built-in field normalization

100+ mappings ship by default. Some highlights:

| Incoming key | Twenty field | |---|---| | phone, phone_number, phoneNumber, tel, mobile, cell | phones.primaryPhoneNumber | | email, email_address, contact_email | emails.primaryEmail | | first_name, firstName, fname | name.firstName | | last_name, lastName, surname | name.lastName | | name, full_name, fullName | name (auto-split) | | company, company_name, business, organization | Company record | | website, url, domain, homepage | domainName.primaryLinkUrl | | utm_source/medium/campaign/content/term | Note (always) | | message, description, notes, comments, analysis | Note (always) |

Unknown fields get an ext_ prefix and are created as custom fields the first time they appear.


Custom field rules

Add IntakeFieldRule records to extend or override the built-in map for a specific source or globally:

| Field | Description | |---|---| | inputPattern | Exact key name or JavaScript regex | | canonicalName | Target field in Twenty (use ext prefix for custom fields) | | fieldType | TEXT, NUMBER, LINKS, EMAILS, PHONES, BOOLEAN, DATE_TIME, NOTE, or SKIP | | priority | Higher = checked first (0–100) |

Rules with no source linked apply globally across all sources.


Source configuration

Each IntakeSource record controls:

| Field | Default | Description | |---|---|---| | targetObject | AUTO | PERSON, COMPANY, or auto-detect | | webhookSecret | — | HMAC-SHA256 signing secret | | createOpportunity | true | Auto-create Opportunity per ingestion | | opportunityNameTemplate | {{source}} — {{firstName}} {{lastName}} | Supports {{source}}, {{firstName}}, {{lastName}}, {{email}}, {{company}} | | status | ACTIVE | Pause a source without deleting it |


Workspace settings

Configurable from Settings → Applications → Intake → Custom:

| Setting | Default | Description | |---|---|---| | INTAKE_APP_LABEL | Intake | Name used in note titles and opportunity names | | INTAKE_DEFAULT_OPP_STAGE | NEW | Stage for auto-created Opportunities | | INTAKE_FIELD_CREATION_ENABLED | true | Toggle auto-schema extension | | INTAKE_MAX_EXT_FIELDS | 50 | Cap on custom fields per object | | INTAKE_DEDUP_WINDOW_MINUTES | 5 | Duplicate suppression window | | INTAKE_REQUIRE_HMAC | false | Enforce signed webhooks globally |


Webhook security

Sign requests with HMAC-SHA256 using the source's secret:

SECRET="your-signing-secret"
PAYLOAD='{"email":"[email protected]"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST https://your-crm.com/s/intake/contact-form \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=$SIGNATURE" \
  -d "$PAYLOAD"

Sources without a secret accept unsigned requests — useful for internal tools. Set INTAKE_REQUIRE_HMAC=true to enforce signatures globally.


Endpoints

| Method | Path | Auth | Description | |---|---|---|---| | POST | /s/intake/:slug | HMAC or open | Ingest a payload | | POST | /s/intake/:slug/test | None | Dry-run — preview without writing | | POST | /s/intake/logs/:logId/retry | API key | Retry a failed ingestion | | GET | /s/intake/health | None | Health check | | POST | /s/intake/sources/register | API key | Register a new source |


Retry failed ingestions

Every ingestion is logged in IntakeLog. Failed logs can be retried from the record's detail page in Twenty, or via API:

curl -X POST https://your-crm.com/s/intake/logs/LOG_ID/retry \
  -H "Authorization: Bearer YOUR_API_KEY"

Development

git clone https://github.com/FranciscoContreras/twenty-app-intake
cd twenty-app-intake
yarn install

# Run unit tests
yarn test

# Connect to your Twenty instance
yarn twenty remote add --api-url https://your-crm.com --api-key YOUR_KEY --as production

# Sync in watch mode
yarn twenty dev

# One-shot sync
yarn twenty dev --once

vs. the alternatives

| | Intake | Zapier/Make | Hookdeck | Custom webhook | |---|---|---|---|---| | Zero config | ✅ | ❌ Manual mapping | ❌ Write ingestion logic | ❌ Build everything | | Auto schema extension | ✅ | ❌ New fields break flows | ❌ | ❌ | | Native Twenty objects | ✅ | ❌ | ❌ | ❌ | | Deduplication | ✅ | Partial | ❌ | Roll your own | | Audit log | ✅ | ❌ | ✅ | ❌ | | Self-hosted | ✅ | ❌ | Paid | ✅ | | Open source | ✅ MIT | ❌ | ❌ | ✅ |


License

MIT — built by Machina · FranciscoContreras