twenty-app-intake 0.2.1 → 0.4.0
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/README.md +293 -8
- package/manifest.json +419 -667
- package/package.json +6 -9
- package/public/cover.generated.png +0 -0
- package/src/front-components/dashboard.mjs +19 -21947
- package/src/front-components/dashboard.mjs.map +4 -4
- package/src/front-components/dashboard.tsx +118 -0
- package/src/front-components/settings-panel.mjs +19 -22103
- package/src/front-components/settings-panel.mjs.map +4 -4
- package/src/front-components/settings-panel.tsx +304 -0
- package/src/front-components/source-panel.mjs +20 -21962
- package/src/front-components/source-panel.mjs.map +4 -4
- package/src/front-components/source-panel.tsx +113 -0
- package/src/logic-functions/check-silence.mjs +263 -0
- package/src/logic-functions/check-silence.mjs.map +7 -0
- package/src/logic-functions/check-silence.ts +52 -0
- package/src/logic-functions/contract.mjs +507 -0
- package/src/logic-functions/contract.mjs.map +7 -0
- package/src/logic-functions/contract.ts +172 -0
- package/src/logic-functions/health.mjs +125 -5
- package/src/logic-functions/health.mjs.map +3 -3
- package/src/logic-functions/health.ts +62 -0
- package/src/logic-functions/quarantine-discard.mjs +380 -0
- package/src/logic-functions/quarantine-discard.mjs.map +7 -0
- package/src/logic-functions/quarantine-discard.ts +65 -0
- package/src/logic-functions/quarantine-list.mjs +211 -0
- package/src/logic-functions/quarantine-list.mjs.map +7 -0
- package/src/logic-functions/quarantine-list.ts +93 -0
- package/src/logic-functions/quarantine-release.mjs +1974 -0
- package/src/logic-functions/quarantine-release.mjs.map +7 -0
- package/src/logic-functions/quarantine-release.ts +94 -0
- package/src/logic-functions/register.mjs +34 -1
- package/src/logic-functions/register.mjs.map +2 -2
- package/src/logic-functions/register.ts +105 -0
- package/src/logic-functions/replay-bulk.mjs +2040 -0
- package/src/logic-functions/replay-bulk.mjs.map +7 -0
- package/src/logic-functions/replay-bulk.ts +83 -0
- package/src/logic-functions/replay-log.mjs +1987 -0
- package/src/logic-functions/replay-log.mjs.map +7 -0
- package/src/logic-functions/replay-log.ts +54 -0
- package/src/logic-functions/retry.mjs +1004 -248
- package/src/logic-functions/retry.mjs.map +4 -4
- package/src/logic-functions/retry.ts +101 -0
- package/src/logic-functions/test-ingest.mjs +616 -28
- package/src/logic-functions/test-ingest.mjs.map +4 -4
- package/src/logic-functions/test-ingest.ts +179 -0
- package/src/logic-functions/webhook.mjs +968 -209
- package/src/logic-functions/webhook.mjs.map +4 -4
- package/src/logic-functions/webhook.ts +58 -0
- package/src/post-install.mjs +34 -1
- package/src/post-install.mjs.map +2 -2
- package/src/post-install.ts +81 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Wire any form, webhook, or data source to Twenty. Leads land clean — every time.</p>
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/twenty-app-intake)
|
|
8
|
-
[](https://twenty.com)
|
|
9
9
|
[](https://github.com/FranciscoContreras/twenty-app-intake/actions)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
[](https://wearemachina.com)
|
|
@@ -63,7 +63,7 @@ Any JSON payload
|
|
|
63
63
|
② Classify short values → CRM fields │ prose / UTMs → note
|
|
64
64
|
│
|
|
65
65
|
▼
|
|
66
|
-
③ Extend unknown fields → auto-create
|
|
66
|
+
③ Extend unknown fields → auto-create ext custom fields on Person, Company or Opportunity
|
|
67
67
|
│
|
|
68
68
|
▼
|
|
69
69
|
④ Deduplicate match by email (Person) or domain (Company) before creating anything
|
|
@@ -113,7 +113,33 @@ curl -X POST https://your-crm.com/s/intake/contact-form/test \
|
|
|
113
113
|
-d '{"first_name":"Jane","email":"jane@co.com","budget":"15000"}'
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
Returns
|
|
116
|
+
Returns a structured diff of what *would* happen, without touching the CRM: per object
|
|
117
|
+
whether it would be **created or updated**, which existing record it matched and on
|
|
118
|
+
what, what each field would do to the value already there, and which fields do not
|
|
119
|
+
exist yet and would be added to the schema. Plus the spam score the payload would get.
|
|
120
|
+
|
|
121
|
+
```jsonc
|
|
122
|
+
{
|
|
123
|
+
"dryRun": true,
|
|
124
|
+
"diff": {
|
|
125
|
+
"objects": [{
|
|
126
|
+
"object": "person",
|
|
127
|
+
"operation": "update",
|
|
128
|
+
"recordId": "8f21…",
|
|
129
|
+
"matchedBy": { "field": "emails.primaryEmail", "value": "jane@co.com" },
|
|
130
|
+
"fields": [
|
|
131
|
+
{ "name": "name", "status": "preserved", "existing": {…}, "incoming": {…} },
|
|
132
|
+
{ "name": "phones", "status": "fill-empty", "incoming": {…} },
|
|
133
|
+
{ "name": "extBudget", "status": "create", "fieldWouldBeCreated": true }
|
|
134
|
+
],
|
|
135
|
+
"fieldsToCreate": [{ "name": "extBudget", "type": "NUMBER" }]
|
|
136
|
+
}]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Because it is machine-readable, this is the gate to put in front of live traffic —
|
|
142
|
+
assert on it in a deployment check rather than reading it by eye.
|
|
117
143
|
|
|
118
144
|
---
|
|
119
145
|
|
|
@@ -188,12 +214,50 @@ Add `IntakeFieldRule` records to extend or override the built-in map for a speci
|
|
|
188
214
|
| Field | Description |
|
|
189
215
|
|---|---|
|
|
190
216
|
| `inputPattern` | Exact key name or JavaScript regex |
|
|
191
|
-
| `canonicalName` | Target field in Twenty
|
|
192
|
-
| `fieldType` | `TEXT`, `NUMBER`, `LINKS`, `EMAILS`, `PHONES`, `BOOLEAN`, `DATE_TIME`, `NOTE`, or `SKIP` |
|
|
217
|
+
| `canonicalName` | Target field in Twenty. An `ext`-prefixed name (`extBudget`) is created automatically; any other name must already exist on the target object |
|
|
218
|
+
| `fieldType` | `TEXT`, `NUMBER`, `LINKS`, `EMAILS`, `PHONES`, `BOOLEAN`, `DATE_TIME`, `CURRENCY`, `NOTE`, or `SKIP` |
|
|
219
|
+
| `targetObject` | `AUTO` (default), `PERSON`, `COMPANY`, or `OPPORTUNITY` |
|
|
220
|
+
| `mergeStrategy` | `INHERIT` (default), `PRESERVE`, or `NEWEST_WINS` — see [Updating existing records](#updating-existing-records) |
|
|
193
221
|
| `priority` | Higher = checked first (0–100) |
|
|
194
222
|
|
|
195
223
|
Rules with no source linked apply globally across all sources.
|
|
196
224
|
|
|
225
|
+
### Writing to the Opportunity
|
|
226
|
+
|
|
227
|
+
Deal attributes — the service someone asked for, the budget they stated, your own
|
|
228
|
+
lead id — belong on the Opportunity, not the contact. Two ways to put them there:
|
|
229
|
+
|
|
230
|
+
**A prefix, no configuration.** Any incoming key beginning `opportunity_`, `opp_`
|
|
231
|
+
or `deal_` is routed to the deal, and the prefix is stripped before the field is
|
|
232
|
+
named — `opportunity_budget` becomes `extBudget` on the Opportunity.
|
|
233
|
+
|
|
234
|
+
```jsonc
|
|
235
|
+
{ "email": "jane@acme.com", "opportunity_budget": "25000", "opportunity_service": "SEO" }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**A rule, for keys you cannot rename.** Set `targetObject: OPPORTUNITY` on the rule
|
|
239
|
+
and point `canonicalName` at the field you want written:
|
|
240
|
+
|
|
241
|
+
| inputPattern | canonicalName | targetObject | fieldType |
|
|
242
|
+
|---|---|---|---|
|
|
243
|
+
| `service` | `machinaService` | `OPPORTUNITY` | `TEXT` |
|
|
244
|
+
| `budget` | `amount` | `OPPORTUNITY` | `CURRENCY` |
|
|
245
|
+
|
|
246
|
+
`amount` and `closeDate` are standard Opportunity fields and are set as the deal is
|
|
247
|
+
created; `amount` accepts a bare number or a written figure (`"$25,000/mo"`) and is
|
|
248
|
+
converted to Twenty's currency micros. Everything else is written as a custom field
|
|
249
|
+
in a follow-up call, so a rejected field never costs you the Opportunity itself.
|
|
250
|
+
|
|
251
|
+
A field routed to the Opportunity by a source that does not create one falls back to
|
|
252
|
+
the primary record, with a warning on the log.
|
|
253
|
+
|
|
254
|
+
### Fields that cannot be written
|
|
255
|
+
|
|
256
|
+
A rule pointing at a field that does not exist — and is not `ext`-prefixed, so cannot
|
|
257
|
+
be auto-created — has its value routed to the note, with a warning naming the field.
|
|
258
|
+
It is not counted as matched. Create the field in Twenty first, or rename the rule's
|
|
259
|
+
`canonicalName` to use an `ext` prefix.
|
|
260
|
+
|
|
197
261
|
---
|
|
198
262
|
|
|
199
263
|
## Source configuration
|
|
@@ -207,6 +271,10 @@ Each `IntakeSource` record controls:
|
|
|
207
271
|
| `createOpportunity` | `true` | Auto-create Opportunity per ingestion |
|
|
208
272
|
| `opportunityNameTemplate` | `{{source}} — {{firstName}} {{lastName}}` | Supports `{{source}}`, `{{firstName}}`, `{{lastName}}`, `{{email}}`, `{{company}}` |
|
|
209
273
|
| `status` | `ACTIVE` | Pause a source without deleting it |
|
|
274
|
+
| `mergePolicy` | — | Overrides `INTAKE_MERGE_POLICY` for this source |
|
|
275
|
+
| `honeypotField` | — | Overrides `INTAKE_HONEYPOT_FIELD` for this source |
|
|
276
|
+
| `expectedCadenceHours` | — | How long this source may go quiet before it counts as silent |
|
|
277
|
+
| `alertWebhookUrl` | — | Posted to once when this source falls silent |
|
|
210
278
|
|
|
211
279
|
---
|
|
212
280
|
|
|
@@ -222,6 +290,206 @@ Configurable from **Settings → Applications → Intake → Custom**:
|
|
|
222
290
|
| `INTAKE_MAX_EXT_FIELDS` | `50` | Cap on custom fields per object |
|
|
223
291
|
| `INTAKE_DEDUP_WINDOW_MINUTES` | `5` | Duplicate suppression window |
|
|
224
292
|
| `INTAKE_REQUIRE_HMAC` | `false` | Enforce signed webhooks globally |
|
|
293
|
+
| `INTAKE_MERGE_POLICY` | `PRESERVE` | What an update does to a field that already has a value |
|
|
294
|
+
| `INTAKE_SPAM_FILTER_ENABLED` | `false` | Score payloads and quarantine at the threshold |
|
|
295
|
+
| `INTAKE_SPAM_SCORE_THRESHOLD` | `5` | Score at which a payload is held |
|
|
296
|
+
| `INTAKE_HONEYPOT_FIELD` | — | Name of a hidden form field that quarantines when filled |
|
|
297
|
+
| `INTAKE_RAW_PAYLOAD_RETENTION` | `FULL` | `FULL` keeps payloads for replay; `NONE` keeps none |
|
|
298
|
+
| `INTAKE_RAW_PAYLOAD_MAX_BYTES` | `65000` | Largest payload stored for replay |
|
|
299
|
+
| `INTAKE_REPLAY_MAX_BATCH` | `50` | Cap on one bulk replay (hard ceiling 500) |
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Updating existing records
|
|
304
|
+
|
|
305
|
+
When a payload matches a contact or company that already exists, `INTAKE_MERGE_POLICY`
|
|
306
|
+
decides what happens to fields that already hold a value.
|
|
307
|
+
|
|
308
|
+
| Policy | Behaviour |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `PRESERVE` *(default)* | Fills fields that are empty, leaves everything else as it is |
|
|
311
|
+
| `NEWEST_WINS` | The incoming payload overwrites — how versions before 0.4.0 behaved |
|
|
312
|
+
|
|
313
|
+
Under both policies a **blank incoming value never overwrites anything**. An absent
|
|
314
|
+
field means the sender had nothing to say about it, not that it should be cleared.
|
|
315
|
+
|
|
316
|
+
`PRESERVE` is the default because the alternative loses data with no record of what
|
|
317
|
+
was there. A returning enquiry typed in lowercase should not replace a name a
|
|
318
|
+
salesperson corrected by hand, and nothing in a CRM undoes a field a webhook
|
|
319
|
+
overwrote at 3am.
|
|
320
|
+
|
|
321
|
+
Override it per source with the source's `mergePolicy`, or per field with a rule's
|
|
322
|
+
`mergeStrategy` — useful for genuinely volatile attributes:
|
|
323
|
+
|
|
324
|
+
| inputPattern | canonicalName | mergeStrategy |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| `lead_score` | `extLeadScore` | `NEWEST_WINS` |
|
|
327
|
+
|
|
328
|
+
The response body reports what the policy did, per object and per field, under
|
|
329
|
+
`mergeDecisions`.
|
|
330
|
+
|
|
331
|
+
**Upgrading from 0.3.0 and want the old behaviour?** Set `INTAKE_MERGE_POLICY=NEWEST_WINS`.
|
|
332
|
+
|
|
333
|
+
> **If a source exists to refresh data, `PRESERVE` will stop it refreshing.**
|
|
334
|
+
> A pipeline that re-scans a business every week and sends back an updated rating,
|
|
335
|
+
> review count or score writes those values once and then never again, because
|
|
336
|
+
> under `PRESERVE` the field already holds a value. This is the one case where the
|
|
337
|
+
> new default is the wrong one. Fix it at whichever scope fits:
|
|
338
|
+
>
|
|
339
|
+
> - the whole source is a refresher → set its `mergePolicy` to `NEWEST_WINS`
|
|
340
|
+
> - only some fields change → give those rules `mergeStrategy: NEWEST_WINS`
|
|
341
|
+
> - every source is a refresher → set `INTAKE_MERGE_POLICY=NEWEST_WINS`
|
|
342
|
+
>
|
|
343
|
+
> Check `mergeDecisions` in the response, or the `Kept the existing …` warnings on
|
|
344
|
+
> the log, to see whether this is happening to you.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Not sending the same lead twice
|
|
349
|
+
|
|
350
|
+
Send an `Idempotency-Key` header (or an `idempotencyKey` field in the body) and a
|
|
351
|
+
repeat of that key resolves to the record made the first time, instead of creating a
|
|
352
|
+
second one:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
curl -X POST https://your-crm.com/s/intake/contact-form \
|
|
356
|
+
-H "Content-Type: application/json" \
|
|
357
|
+
-H "Idempotency-Key: submission-8f21c9" \
|
|
358
|
+
-d '{"email":"jane@acme.com"}'
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Unlike the content-hash deduplication — which only looks back
|
|
362
|
+
`INTAKE_DEDUP_WINDOW_MINUTES` — an idempotency key has **no time limit**. A
|
|
363
|
+
double-tapped submit button, a client retrying after a timeout and a queue
|
|
364
|
+
redelivering an hour later all resolve to the same record.
|
|
365
|
+
|
|
366
|
+
A key whose only previous use was quarantined or discarded is treated as unused, so
|
|
367
|
+
a released payload is not blocked by its own earlier attempt.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Spam quarantine
|
|
372
|
+
|
|
373
|
+
Off by default. Two mechanisms, and they work independently.
|
|
374
|
+
|
|
375
|
+
**Honeypot** — set `INTAKE_HONEYPOT_FIELD` (or a source's `honeypotField`) to the
|
|
376
|
+
name of a form field hidden from people by CSS. Any payload arriving with it filled
|
|
377
|
+
was filled by a script, and is quarantined immediately. No false positives, so this
|
|
378
|
+
works whether or not scoring is enabled.
|
|
379
|
+
|
|
380
|
+
**Scoring** — set `INTAKE_SPAM_FILTER_ENABLED=true`. Each signal is worth points
|
|
381
|
+
rather than a verdict: a URL in a name field, a disposable or undeliverable email
|
|
382
|
+
domain, a link blast in the message, one long string pasted into every box, a
|
|
383
|
+
placeholder phone number. A payload is held when the total reaches
|
|
384
|
+
`INTAKE_SPAM_SCORE_THRESHOLD` (default `5`), so no single signal is enough on its own
|
|
385
|
+
— a genuine lead writing from a throwaway address still gets through.
|
|
386
|
+
|
|
387
|
+
Signals that would score *people* rather than behaviour are deliberately absent.
|
|
388
|
+
Non-Latin characters in a name and industry words like "SEO" carry no penalty.
|
|
389
|
+
|
|
390
|
+
A quarantined payload creates **no Person, Company, Opportunity or Note**. It is
|
|
391
|
+
recorded as an `IntakeLog` with status `QUARANTINED`, its score and its reasons, and
|
|
392
|
+
the webhook answers `202` — telling a bot which attempts were caught only teaches it
|
|
393
|
+
what to change, and a real person should not see a failure on a form that in fact
|
|
394
|
+
went through.
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# What is being held, and why
|
|
398
|
+
curl https://your-crm.com/s/intake/quarantine -H "Authorization: Bearer $KEY"
|
|
399
|
+
|
|
400
|
+
# Let one through — the filter is overruled, the score is still recorded
|
|
401
|
+
curl -X POST https://your-crm.com/s/intake/quarantine/$LOG_ID/release -H "Authorization: Bearer $KEY"
|
|
402
|
+
|
|
403
|
+
# Mark one as junk; add {"purgePayload":true} to drop the stored body
|
|
404
|
+
curl -X POST https://your-crm.com/s/intake/quarantine/$LOG_ID/discard -H "Authorization: Bearer $KEY"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Turn scoring on only after watching the `spamScore` on a few days of real logs.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Replay: applying a mapping you added too late
|
|
412
|
+
|
|
413
|
+
Every payload is stored on its log, so a rule written after the fact can be applied
|
|
414
|
+
to everything already received.
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# One log, through the rules as they are now
|
|
418
|
+
curl -X POST https://your-crm.com/s/intake/logs/$LOG_ID/replay -H "Authorization: Bearer $KEY"
|
|
419
|
+
|
|
420
|
+
# A batch — see what would be touched first
|
|
421
|
+
curl -X POST https://your-crm.com/s/intake/replay \
|
|
422
|
+
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
|
|
423
|
+
-d '{"sourceSlug":"contact-form","since":"2026-08-01T00:00:00Z","dryRun":true}'
|
|
424
|
+
|
|
425
|
+
# Then run it
|
|
426
|
+
curl -X POST https://your-crm.com/s/intake/replay \
|
|
427
|
+
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
|
|
428
|
+
-d '{"sourceSlug":"contact-form","since":"2026-08-01T00:00:00Z","limit":50}'
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Replay accepts logs that already **succeeded** — that is the point, since the
|
|
432
|
+
ingestion worked and only the mapping was missing. Retry is the narrower operation
|
|
433
|
+
and still refuses a successful log.
|
|
434
|
+
|
|
435
|
+
Replays run one at a time and are capped by `INTAKE_REPLAY_MAX_BATCH`, because each
|
|
436
|
+
one writes to the CRM. Each new log records `replayOfLogId`, so a re-mapped record
|
|
437
|
+
traces back to the payload it came from.
|
|
438
|
+
|
|
439
|
+
### What is stored, and what is not
|
|
440
|
+
|
|
441
|
+
`INTAKE_RAW_PAYLOAD_RETENTION=FULL` (the default) keeps each payload as sent, which
|
|
442
|
+
is what retry and replay run from. `NONE` keeps nothing and disables both.
|
|
443
|
+
|
|
444
|
+
Under either setting, **credentials are never stored** — keys containing `password`,
|
|
445
|
+
`token`, `secret`, `apikey`, `authorization`, `cvv`, `card`, `ssn` and similar are
|
|
446
|
+
replaced with `[redacted]` before the payload is written.
|
|
447
|
+
|
|
448
|
+
Everything else the sender submitted **is** kept, including names, emails and phone
|
|
449
|
+
numbers. It lives on the `IntakeLog` object under your workspace's own access
|
|
450
|
+
control and is readable by anyone who can read that object. Payloads over
|
|
451
|
+
`INTAKE_RAW_PAYLOAD_MAX_BYTES` are ingested normally but not stored — a truncated
|
|
452
|
+
payload cannot be parsed, so it is dropped rather than half-kept, and the log says so.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Knowing when a source goes quiet
|
|
457
|
+
|
|
458
|
+
The failure nobody notices is the one that produces no error: a form that breaks in
|
|
459
|
+
February and is found in August, with nothing but absent leads as evidence.
|
|
460
|
+
|
|
461
|
+
Give a source an `expectedCadenceHours` and it becomes monitored. Sources without
|
|
462
|
+
one are never flagged — silence is only a fault where traffic was expected.
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# Evaluate every source; run this on whatever timer you already have
|
|
466
|
+
curl -X POST https://your-crm.com/s/intake/sources/check-silence -H "Authorization: Bearer $KEY"
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The check records `healthStatus` (`HEALTHY`, `SILENT`, `NEVER_RECEIVED`) and
|
|
470
|
+
`silentSince` on each source, and posts once to the source's `alertWebhookUrl` on
|
|
471
|
+
the transition into silence — once, not on every check. The body carries a `text`
|
|
472
|
+
key, so Slack, Discord and Teams incoming webhook URLs work unchanged.
|
|
473
|
+
|
|
474
|
+
`GET /s/intake/health` also reports silent sources. It still returns `200` and the
|
|
475
|
+
same `status` and `timestamp` keys it always did, so existing monitors are
|
|
476
|
+
unaffected. Point a monitor at `/s/intake/health?strict=true` to get a `503` when a
|
|
477
|
+
source has fallen silent.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Asking the app what it accepts
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
curl https://your-crm.com/s/intake/contract -H "Authorization: Bearer $KEY"
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
One call returns every endpoint, the built-in field map grouped by destination, the
|
|
488
|
+
custom fields that currently exist on each object, the active rules, every
|
|
489
|
+
registered source and the settings in force — so an integrator or an agent can learn
|
|
490
|
+
the contract without reading source or introspecting Twenty's metadata API.
|
|
491
|
+
|
|
492
|
+
Signing secrets never appear; a source reports only whether it requires a signature.
|
|
225
493
|
|
|
226
494
|
---
|
|
227
495
|
|
|
@@ -249,10 +517,27 @@ Sources without a secret accept unsigned requests — useful for internal tools.
|
|
|
249
517
|
| Method | Path | Auth | Description |
|
|
250
518
|
|---|---|---|---|
|
|
251
519
|
| `POST` | `/s/intake/:slug` | HMAC or open | Ingest a payload |
|
|
252
|
-
| `POST` | `/s/intake/:slug/test` | None | Dry-run —
|
|
253
|
-
| `
|
|
254
|
-
| `GET` | `/s/intake/
|
|
520
|
+
| `POST` | `/s/intake/:slug/test` | None | Dry-run — structured diff, writes nothing |
|
|
521
|
+
| `GET` | `/s/intake/health` | None | Health check, plus silent sources |
|
|
522
|
+
| `GET` | `/s/intake/contract` | API key | What the app accepts and how it is configured |
|
|
255
523
|
| `POST` | `/s/intake/sources/register` | API key | Register a new source |
|
|
524
|
+
| `POST` | `/s/intake/sources/check-silence` | API key | Check every source against its cadence |
|
|
525
|
+
| `POST` | `/s/intake/logs/:logId/retry` | API key | Retry a **failed** ingestion |
|
|
526
|
+
| `POST` | `/s/intake/logs/:logId/replay` | API key | Re-run **any** stored payload through current rules |
|
|
527
|
+
| `POST` | `/s/intake/replay` | API key | Bulk replay a selection of logs |
|
|
528
|
+
| `GET` | `/s/intake/quarantine` | API key | List held payloads and why |
|
|
529
|
+
| `POST` | `/s/intake/quarantine/:logId/release` | API key | Ingest a held payload |
|
|
530
|
+
| `POST` | `/s/intake/quarantine/:logId/discard` | API key | Mark a held payload as junk |
|
|
531
|
+
|
|
532
|
+
### Response codes
|
|
533
|
+
|
|
534
|
+
| Code | Meaning |
|
|
535
|
+
|---|---|
|
|
536
|
+
| `200` | Ingested, or a duplicate resolved to the original record |
|
|
537
|
+
| `202` | Held for review by the spam filter — nothing was created |
|
|
538
|
+
| `401` | Signature missing or invalid |
|
|
539
|
+
| `404` | No source with that slug |
|
|
540
|
+
| `423` | Source is paused |
|
|
256
541
|
|
|
257
542
|
---
|
|
258
543
|
|