Skip to content

Recipe — Sync new user to CRM

The moment a user finishes registration, post their details straight to your CRM. Sales sees the lead within seconds; you skip the batch sync entirely.

Before you begin
  • You are signed in as a tenant admin with permission to edit flows and custom actions.
  • You have read Flows concepts — Flow, Stage, Block, and Action should be familiar terms.
  • Your CRM exposes an HTTPS endpoint that accepts a JSON POST. For development you can substitute webhook.site.
  • The CRM's domain is on your tenant's fetch allowlist (Security settings). If it isn't, the api.fetch call inside the action will be rejected before any request leaves the platform.

Every new signup triggers a custom action that posts the user's metadata to your CRM's intake endpoint. The action runs server-side, inside the Registration flow, immediately after the account is created — not on a schedule, not in a queue, right then.

This recipe fits any B2B or sales-assisted product where you want the sales team to know about a new signup before the user's welcome email even arrives. It also works for self-serve products that route free signups into a nurture sequence — just swap the CRM endpoint for your marketing automation intake URL.

If your CRM supports direct webhooks from the platform's Call Webhook block, you can do this without writing code — see Block reference — Notification & Integration. Use a custom action (this recipe) when you need to shape the payload, handle errors gracefully, or set metadata on the user object at the same time.

A Registration flow with a Post-Create stage containing a Run Custom Action block wired to a new action called sync-new-user-to-crm. The action:

  1. Reads the new user's ID, email, signup country, tier, and creation timestamp from event.*.
  2. Builds a CRM-shaped payload.
  3. POSTs it to your endpoint via api.fetch.
  4. Logs success; catches and logs errors without denying the registration.
Post-Create stage
└─ Block: Run Custom Action (action: sync-new-user-to-crm)

The user's account is created before this stage runs. A failure here never undoes the signup.

Open webhook.site in a new tab. Copy the unique URL it generates — it looks like https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Leave the tab open; every POST the action fires will appear there.

In the admin console sidebar click Flows, then select the Custom Actions tab. Click + New Action and fill in the dialog:

  • Namesync-new-user-to-crm
  • Trigger — Registration
  • DescriptionPosts new user metadata to the CRM intake endpoint on every signup

Click Create. The code editor opens.

Replace the starter template with the code in the next section, then click Save.

Click Flows → Registration → Edit. Locate the Post-Create stage. Drag a Run Custom Action block into that stage. Open its config panel and pick sync-new-user-to-crm from the action selector. Click Save flow.

Paste this into the action editor. Read the inline comments — they explain the two decisions that matter.

// sync-new-user-to-crm
//
// Posts a compact user record to the CRM intake endpoint on every new signup.
// Fails open: a CRM outage never blocks registration.
const CRM_ENDPOINT = 'https://webhook.site/YOUR-UUID-HERE'; // swap before go-live
async function main() {
// Pull the fields we want. event.user.app_metadata may be empty on first signup;
// the nullish coalescing below keeps the payload well-formed either way.
const userId = event.user.id;
const email = event.user.email;
const signupAt = event.user.created_at;
const country = event.request.geo?.country ?? 'unknown';
const tier = event.user.app_metadata?.tier ?? 'free';
const payload = {
user_id: userId,
email: email,
tier: tier,
signup_country: country,
signup_at: signupAt,
};
try {
const res = await api.fetch(CRM_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant': event.tenant.slug,
},
body: JSON.stringify(payload),
});
if (res.status >= 200 && res.status < 300) {
api.log('info', `crm-sync posted for ${userId} (status ${res.status})`);
} else {
// Log the failure but do not deny — the CRM is downstream of signup.
api.log('warn', `crm-sync non-2xx for ${userId}: status ${res.status}`);
}
} catch (err) {
// Network error or fetch-allowlist rejection. Log and continue.
api.log('error', `crm-sync failed for ${userId}: ${err.message}`);
}
}
main();

Two things to note. First, api.fetch is the only way to make an outbound HTTP call from an action — direct fetch is not available. Second, notice there is no api.deny call anywhere. The CRM is downstream of the signup decision; a failed sync should never take down a user's registration.

Open the action's Test pane (the beaker icon in the editor toolbar). The pane has preset inputs for common flow contexts.

Select New US user from the preset list. This populates event.user.*, event.request.geo.country, and related fields with synthetic values. Click Run.

After the run completes, the Test output panel shows:

  • The API calls tab — expect one api.fetch entry pointing at your webhook.site URL, with the request body shown.
  • The Logs tab — expect an info entry: crm-sync posted for usr_... (status 200).

Switch to your webhook.site tab. The POST should have arrived with the payload: user_id, email, tier, signup_country, and signup_at all filled in with the preset values.

If you see an error log instead, the most likely cause is that webhook.site (or your CRM domain) is not yet on your tenant's fetch allowlist. Go to Security → Outbound URL allowlist and add the domain, then re-run.

Sign up with a fresh test email on your tenant's user-facing registration URL. Use a synthetic email address — something like test+crm-recipe-01@example.com.

After the registration completes, switch to webhook.site. Within a second or two, a POST should arrive. Inspect the request body and confirm:

  • user_id starts with usr_
  • email matches the address you registered with
  • signup_country shows the two-letter country code for your IP

In the admin console, go to Flows → Registration → Recent Runs. Click the most recent run. Find the Run Custom Action block in the trace and confirm the log line crm-sync posted for usr_... is present.

Once verified, replace the CRM_ENDPOINT constant in the action with your real CRM intake URL, save the action, and republish the Registration flow.

  • Never api.deny from this action. The CRM is downstream of account creation. A failed POST to sales should not undo a user's registration. The try/catch block in the code above ensures this — leave it in place.
  • Idempotency. If a user retries the registration form after an error, the action may fire more than once for the same email. webhook.site will show both requests. Your production CRM should de-duplicate by email address or user_id; the platform does not deduplicate outbound calls.
  • Payload size. Keep the payload compact. The api.fetch call has a request body size limit; sending large blobs (many kilobytes of metadata) may cause the call to fail. Stick to the scalar fields shown above and add more only when your CRM actually consumes them.
  • Fetch allowlist required. Only hostnames on your tenant's fetch allowlist can be reached from a custom action. The rejection happens before any network packet leaves the platform, so the error in the Test pane will say http_fetch_denied, not a connection timeout. Add the domain in Security → Outbound URL allowlist before testing against your production CRM.
  • Signing requests for production. webhook.site accepts everything; your real CRM endpoint should verify that the request came from your tenant. A common pattern is to include a shared-secret HMAC signature in a header (X-Signature) and verify it on arrival. That technique is documented in Custom actions — API reference, which covers api.fetch headers in detail.