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.
- 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.fetchcall 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.
When to use it
Section titled “When to use it”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.
What you'll build
Section titled “What you'll build”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:
- Reads the new user's ID, email, signup country, tier, and creation timestamp from
event.*. - Builds a CRM-shaped payload.
- POSTs it to your endpoint via
api.fetch. - 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.
Configure it
Section titled “Configure it”1. Get a test webhook URL
Section titled “1. Get a test webhook URL”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.
Anyone with the inspector URL can read what your Flow posts there. Use a tenant test user and fake email when developing against webhook.site or beeceptor.com; switch to your real backend URL before going live. Treat the inspector URL itself as semi-sensitive — anyone you share it with can see the requests too.
2. Create the custom action
Section titled “2. Create the custom action”In the admin console sidebar click Flows, then select the Custom Actions tab. Click + New Action and fill in the dialog:
- Name —
sync-new-user-to-crm - Trigger — Registration
- Description —
Posts 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.
3. Add the block to the Registration flow
Section titled “3. Add the block to the Registration flow”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.
The code
Section titled “The code”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.
Test it
Section titled “Test it”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.fetchentry pointing at your webhook.site URL, with the request body shown. - The Logs tab — expect an
infoentry: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.
Verify on a live flow
Section titled “Verify on a live flow”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_idstarts withusr_emailmatches the address you registered withsignup_countryshows 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.
Cautions
Section titled “Cautions”- Never
api.denyfrom 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.fetchcall 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 coversapi.fetchheaders in detail.