Pull a public feed into a CRM in four timed stages.

For owners who copy records between apps: pick GitHub, Wikipedia or Hacker News and watch each stage's time. 200+ recorded pulls, median 0.40 s.

Pull from a public API

loads with the page

Source

This demo needs JavaScript. It makes real requests to a real server, so there is no static version to show.

  1. Extract–Ask the API
  2. Normalize–One record shape
  3. Dedupe–Merge repeats
  4. Upsert–Write to CRM

Waiting for a run.

Whole pull
–

Each stage’s time appears as it finishes.

195 recorded pulls from public feeds since 30 Oct 2026, median 0.40 s from request to saved record. Counted

Pull

Someone on your team is the go-between for two apps. An email lead arrives, someone copies it across by hand, and now and then a record goes missing. Worse is a sync you cannot trust: it works until it quietly stops. We connect the two so the copying stops. Below, one connection runs live on real public data.

Server-sent events [a live feed from the server], exactly as they arrive: the pipeline’s stages and your webhook’s verdicts.

Logno run yet

Run the pipeline or send a webhook. Each event logs here as it arrives.

Raw JSON of the last pull
{}

This is a live run. The server has recorded 200+ pulls from public feeds into a small CRM (the tool where you keep leads), and a scheduled job keeps adding more. The median total is 0.40 s for the four stages combined.

  1. Pick GitHub public events, Wikipedia recent edits or Hacker News new stories.
  2. Run the pull. The server fetches the feed, puts each record in one shape, drops repeats and saves it.
  3. Read the time of each stage. [extract, normalize, dedupe, upsert]

A first job is this small. Name two tools and one record, such as a new form lead going into your CRM.

Webhook

A vendor posts a message to you, and each one has to be genuine and counted once. That kind of message is a webhook. Without a signature check, anyone who finds the address can post to it.

  1. Create a personal receiving address. The demo keeps it for 30 minutes. [endpoint]
  2. Send a correctly signed event, then one with a forged signature. The demo accepts the first and refuses the second.
  3. Send the same event identifier again and watch the demo ignore it.
  4. Read the demo's answer to each event: accepted, refused or ignored.

Only messages signed with your secret get in. The rest are refused and logged, and each refusal shows on your own live event stream with its reason. [Server-Sent Events]

For your tools, we wire one direction against a test copy first. You see each step, its time and what it refused. Each connection starts with a practice run that changes nothing, accepts signed messages only and applies a repeat once. If it holds up, it goes live with a pause switch and a log you can read. You decide, then we switch it on.

Receive a signed webhook

A real endpoint made just for you. It disappears after 30 minutes.

What the webhook does with a bad message. Try each one with the keys above or your own curl.
What you sendWhat happensAnswer
A forged or wrong signatureRefused before the body is read as data.401 signature_mismatch
A real message, body changed after signingThe signature no longer matches. Refused.401 signature_mismatch
A genuine old message, replayed laterSignature fine, but older than 5 minutes. Refused.401 timestamp_out_of_tolerance
The same event id twiceRecognised, nothing written a second time.200 duplicate: true
A valid signature, bad dataRejected with the field that is wrong.400 invalid_payload
A body that is not JSON at allStopped by the shared server before the webhook code runs. It never reaches the CRM or your stream.400 bad_json
More than 8 KBRejected before it is processed.413
Too many requestsSlowed down, with a time to wait.429 Retry-After

The CRM it writes to

Your form, your CRM and your billing tool each know something the others do not. A lead then reaches the CRM when the message arrives instead of when someone finds time to copy it. A message that arrives twice is counted once. We also match one company spelled three ways on a clean key, so it becomes one record.

A sync can also stop quietly when the other app is slow or says no, so a failure has to be reported and retried.

The server cleans each pulled record, removes repeats and saves it in a small database that holds public feed handles only, no emails. [SQLite]

Parsing rejects the broken shapes the three APIs can return: deleted items, bots, malformed rows. Each run reports how many records it rejected and why.

  • 200+ Pulls recorded Showpiece status Checked
  • 0.40 s Median total time, four stages combined Showpiece status Checked
  • 4,743 and 922 Contacts and organizations in the demo CRM Showpiece status Checked

The CRM, and the time each stage takes

loads with the page

The CRMcounts load with the page

Most recently changed contacts. Public handles are shortened on this page; contacts you create through your own webhook show in full, to you only.
ContactCompany or siteSourceTouches
The contacts load from the demo server with the page.

Measured latencyhistograms from real runs

Loads with the page.

Guardrails built in

Each internet address can start 8 runs per 10 minutes, and a personal receiving address lasts 30 minutes. The counts above grow as people use the demo.

GitHub allows 60 anonymous requests an hour; the demo shows how many are left and when they reset, and keeps the last 8 in reserve.

If GitHub was asked less than a minute ago, the server reuses its answer and the page says so. After that, it asks GitHub whether anything changed, and a 304 reply means no.

When one feed refuses or slows down, you see the reason and the time it resets. Everything else keeps working: the other two feeds still run.

The feeds are public, and the demo writes only to the small CRM shown here.

The check at the door

Skip the technical part

This part is for the technical reader.

A forged message must fail before it changes a record. In the message-handling code, the signature covers the timestamp and the body. The check accepts only messages inside a 300-second window. [HMAC-SHA256]

The code recomputes the signature from the timestamp and the body, then compares it with the one sent, in constant time. The receiver refuses every message if no secret is configured, so an unset secret cannot leave the address open.

The receiver saves each message before processing it. A unique index on the source and event identifier makes a repeated delivery apply once, and a failure is reported so the sender retries. [unique index]

This demo's own signature follows the pattern Stripe documents: the timestamp and the raw body are signed together, inside a five-minute window. Other signature versions are ignored, so the check cannot be downgraded. [HMAC-SHA256]

demo-api/routes/showpiece.js, lines 109 to 120 The demo server's own check: the signature first, compared in constant time, then the 300-second window.
  const expected = Buffer.from(sign(secret, p.t, rawBody), 'hex');
  let match = false;
  for (const cand of p.v1) {
    // every candidate is exactly 32 bytes (regex above), so timingSafeEqual cannot throw; no early exit
    if (crypto.timingSafeEqual(Buffer.from(cand, 'hex'), expected)) match = true;
  }
  if (!match) return { ok: false, reason: 'signature_mismatch', t: p.t };
  // signature first, window second: a forger learns nothing about the window
  const skewSec = nowSec - p.t;
  if (Math.abs(skewSec) > toleranceSec) return { ok: false, reason: 'timestamp_out_of_tolerance', t: p.t, skewSec };
  return { ok: true, t: p.t, skewSec };
}

Tell us what you would change

Name two tools and one record. That could be a retyped email lead, one company spelled three ways, a message that arrives twice, or a sync a customer noticed was broken.

Start a project