Track events. We handle the rest. Pre-built workflows recover payments, convert trials, and re-engage users, optimized across thousands of companies.
Call fbf.track() when things happen
Pre-built workflows match your events
Right message, right channel, right time
Payments saved, trials converted, users retained
Each vertical has pre-built workflows optimized for your industry. Pick yours to see relevant events and examples.
Subscription recovery, trial conversion, upsells
payment.failedtrial.started+2trial.ending+1 moreCart recovery, order updates, reviews
cart.abandonedorder.placed+2order.delivered+1 morePayment reminders, KYC completion, reactivation
payment.duetransaction.failed+2kyc.incomplete+1 moreCourse engagement, streak recovery, certificates
course.enrolledcourse.abandoned+2streak.broken+1 morePlayer win-back, IAP prompts, achievements
player.inactivecurrency.low+2level.completed+1 moreTrial conversion, subscription recovery, reviews
trial.startedtrial.ending+2subscription.canceled+1 moreInstall the SDK and initialize for your vertical.
Select your vertical
1. Install the SDK
npm install @fourbyfour/sdk2. Initialize for your vertical
import { saas } from '@fourbyfour/sdk'; const fbf = saas({ apiKey: process.env.FOURBYFOUR_API_KEY, projectId: process.env.FOURBYFOUR_PROJECT_ID});Get your API key from the dashboard. Each vertical gives you typed events and pre-built workflows optimized for your industry.
Two functions. That's all you need.
Select your vertical
// Payment failed → triggers recovery workflowawait fbf.track('payment.failed', { userId: 'u_123', amount: 99, currency: 'USD', plan: 'Pro', subscriptionId: 'sub_456', billingCycle: 'monthly'});// Send context to help optimize deliveryawait fbf.notify({ userId: 'u_123', timezone: 'America/New_York', preferredChannel: 'email', tier: 'premium'});When you call track('payment.failed'), our Payment Recovery workflow kicks in automatically. We handle timing, channel selection, message optimization, all tuned from patterns across thousands of companies.
Answer a few questions about your stack and we'll generate a step-by-step integration guide that you can feed to your AI coding assistant (Claude Code, Cursor, Copilot, etc.).
We'll generate code examples in your language.
YOUR_PROJECT_ID and YOUR_API_KEY with your actual values from the dashboardTwo functions. No PII. That's the entire SDK.
Select your vertical
fbf.track(event, payload)Events that trigger workflows
import { saas } from '@fourbyfour/sdk'; const fbf = saas({ apiKey: process.env.FOURBYFOUR_API_KEY, projectId: process.env.FOURBYFOUR_PROJECT_ID}); // Minimum: just userId (we use defaults)await fbf.track('payment.failed', { userId: 'u_123'}); // Full: everything you know (better optimization)await fbf.track('payment.failed', { userId: 'u_123', amount: 99, currency: 'USD', plan: 'Pro', subscriptionId: 'sub_456', billingCycle: 'monthly', failureReason: 'card_declined', attemptNumber: 1});fbf.notify(context)Context that helps us optimize
// Send context anytime to help us optimize deliveryawait fbf.notify({ userId: 'u_123', timezone: 'Asia/Kolkata', // When to reach them tier: 'premium', // Priority signals preferredChannel: 'whatsapp', // Channel preference language: 'en', // Localization ltv: 500, // Value signals converted: true // Feedback after conversion});Works out of the box. We use intelligent defaults optimized across thousands of companies.
await fbf.track('payment.failed', { userId: 'u_123'});Better optimization. More context = smarter timing, channels, and messaging.
await fbf.track('payment.failed', { userId: 'u_123', amount: 99, plan: 'Pro', // ... everything helps});The key: Send what you have, when you have it. No need to block on missing fields. We fill gaps with smart defaults, then improve as you send more.
trackNo PIIBusiness events: amount, plan, cartTotal
notifyNo PIIUser context: timezone, tier, preferences
resolverTransientDelivery info: email, phone, name
| Function | Data Type | Example | PII? |
|---|---|---|---|
track | Business events | amount, plan, cartTotal | No |
notify | User context | timezone, tier, preferences | No |
resolver | Delivery info | email, phone, name | Yes (transient) |
Notice: PII only flows through your resolver endpoint, fetched just-in-time, used for delivery, never stored. See the Resolver section.
Just-in-time PII. Your data stays in your system until the moment of delivery.
User event matches a workflow
Request only needed fields
Email, phone, name: what's needed
Used for send, never stored
We send:
POST /api/fourbyfour/resolveHeaders: x-fbf-signature: sha256=... Body:{ "userId": "u_123", "fields": ["email", "name"], "workflowId": "payment-recovery", "channel": "email"}You respond:
HTTP 200 OK { "email": "user@example.com", "name": "John"} // Or if user not found:HTTP 404 Not Found{ "error": "User not found"}// Express / Next.js API routeapp.post('/api/fourbyfour/resolve', async(req, res) => { // Verify the request is from Fourbyfour const signature = req.headers['x-fbf-signature']; if(!verifySignature(signature, req.body, process.env.FBF_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } const { userId, fields, workflowId, channel } = req.body; // Fetch user from your database const user = await db.users.findById(userId); if(!user) { return res.status(404).json({ error: 'User not found' }); } // Return only requested fields const response: Record<string, string> = {}; if(fields.includes('email')) response.email = user.email; if(fields.includes('name')) response.name = user.firstName; if(fields.includes('phone')) response.phone = user.phone; res.json(response);});email+ namephone+ namephone+ namepushToken+ name, device| Channel | Required | Optional |
|---|---|---|
email | name | |
| SMS | phone | name |
phone | name | |
| Push | pushToken | name, device |
63 pre-built workflows optimized across thousands of companies. Just track the event. We handle timing, channels, and messaging.
SaaS: Convert trials based on how they're using the product
Track event
Call fbf.track()
Workflow triggers
Matches your event
We optimize
Timing, channel, message
Revenue recovered
Payments, trials, users
We deliver via the optimal channel. You just provide the resolver endpoint. We handle the rest.
Best for: detailed info, rich content
email
Best for: urgent, time-sensitive
phone
Best for: conversational, global
phone
Best for: real-time, mobile
pushToken
If you sent preferredChannel via notify(), we start there.
We consider timezone, urgency, past engagement, and message type.
We know "users in this segment convert 2x better via WhatsApp".
User's timezone is IST
Past engagement higher on SMS
User received message yesterday
Payment fails in 24h
| Signal | Example | Impact |
|---|---|---|
| Timing | User's timezone is IST | Send at 10am local time |
| Channel | Past engagement higher on SMS | Use SMS over email |
| Frequency | User received message yesterday | Wait before next touch |
| Urgency | Payment fails in 24h | Escalate to SMS |
If a channel fails or isn't available, we automatically try the next best option: