Developer Quickstart: Link UI + Unified Webhooks for B2B SaaS
Deploy secure OAuth flows and ingest normalized third-party data in days. Combine an embedded Link UI with unified webhooks to ship integrations without per-provider code.
Your product manager just promised a key prospect a HubSpot sync by the end of the quarter. Your senior backend engineer estimates six weeks for the OAuth flow alone, another four weeks to build webhook signature verification across three different providers, and an unknown amount of time for the inevitable token-refresh fires that will trigger PagerDuty alerts at 3 AM. The deal closes in three weeks.
You are a startup CTO or engineering leader staring at a spreadsheet of lost deals, and the pattern is painfully obvious. Prospects run a successful evaluation, love your core product, and then ask the inevitable question: "Do you integrate with our HRIS and our CRM?" You reply that it is on the roadmap. By the time your engineering team ships it, the deal is completely cold.
Building SaaS integrations in-house is a massive capital expense. Organizations typically spend $30,000 to $500,000+ just for the initial build of their integration infrastructure, with annual maintenance running 15 to 25% of that original cost. That is merely the hard build number. The hidden cost is opportunity: every hour your senior engineers spend debugging a broken Salesforce custom field mapping or deciphering a cryptic OAuth error is an hour they are not building your core product.
This dynamic creates an efficiency squeeze. The median B2B SaaS company now spends $2.00 in sales and marketing to acquire just $1.00 of new customer ARR. You cannot afford to hemorrhage six-figure enterprise deals because your application lacks connectivity.
This quickstart is a technical blueprint for solving the integration bottleneck. We will bypass the months-long nightmare of building custom OAuth flows and provider-specific webhook parsers. Instead, we will implement a unified architecture. By the end of this guide, you will know exactly how to drop in a secure authentication UI and start receiving normalized, enriched third-party data via a single webhook endpoint. This is the "Tuesday to Friday" integration—the architecture that unblocks sales deals in days, not quarters.
The Integration Ultimatum: Speed vs. Engineering Debt
Traditional integration development forces teams into a false dichotomy. You can either build deep, native integrations that take months of engineering time, or you can offload the problem to visual workflow builders that frustrate your end-users and break under enterprise data volumes.
A unified integration quickstart is a developer flow where customer authentication, OAuth token management, and third-party webhook ingestion are abstracted behind two surfaces: a drop-in UI component and a single normalized event stream. Done right, a five-person team can ship a customer-facing CRM, HRIS, or ticketing integration in days instead of quarters.
The job-to-be-done in this quickstart is narrow but highly impactful:
- Let an end customer connect their third-party account securely in under 60 seconds.
- Receive a verified, structured webhook for every change in that account.
- Never wake up your on-call engineers because a refresh token rotated or expired.
Authentication and asynchronous data ingestion (webhooks) remain the two most massive hurdles in integration engineering. OAuth 2.0 is notorious for edge cases, token expiration failures, and security vulnerabilities like CSRF attacks. Webhooks are even worse. There is no standard webhook security; providers use vastly different methods like HMAC-SHA256, JWTs, or custom challenge handshakes. We are going to solve both problems simultaneously.
Step 1: Embedding the Link UI for Seamless OAuth
Authentication is the first point of failure in any integration. If you build this in-house, you must manage redirect URIs, secure state parameters to prevent CSRF, implement PKCE code verifiers, securely encrypt access tokens at rest, and build background workers to refresh those tokens before they expire.
The Link UI is the entry point that abstracts this entire lifecycle. It is a drop-in frontend component you mount into your product that handles the entire third-party authentication ceremony: provider selection, OAuth redirects, scope negotiation, API key collection, and the secure handoff of credentials to the backend. It completely removes the need to build custom frontend components or secure credential vaults in your own database.
The Implementation
To initiate the connection process, you generate a short-lived link token from your backend and pass it to the frontend component.
Backend (Node.js):
// Create a short-lived link token from your backend
const response = await fetch('https://api.truto.one/link-token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TRUTO_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
tenant_id: 'customer_12345', // Your internal customer ID
environment_id: 'env_production',
integrations: ['hubspot', 'salesforce', 'pipedrive'] // Limit available providers
})
});
const { token } = await response.json();Frontend (React):
import { TrutoLink } from '@truto/link-react';
export function IntegrationSettings({ linkToken }) {
return (
<TrutoLink
token={linkToken}
onSuccess={(integratedAccount) => {
console.log('Successfully connected:', integratedAccount.id);
// Store the integratedAccount.id against your user/tenant in your DB
saveIntegratedAccountId(integratedAccount.id);
}}
onError={(error) => {
console.error('Connection failed:', error);
}}
/>
);
}What Happens Under the Hood
When a user completes the OAuth flow via the Link UI, the platform securely exchanges the authorization code for an access token and a refresh token. These credentials are encrypted at rest using AES-256-GCM.
More importantly, the backend behind the Link UI quietly assumes responsibility for the boring-but-critical token lifecycle work. The platform uses distributed scheduling mechanisms to proactively refresh OAuth access tokens shortly before they expire, utilizing a safety buffer to prevent in-flight requests from failing on a stale token. Concurrent refresh attempts are serialized so multiple in-flight API calls do not stampede the provider's token endpoint.
If a refresh fails permanently because the customer revoked access in their provider settings, the account is flagged as needing reauthentication and an integrated_account:authentication_error webhook event is fired to your system so your app can prompt the user. You never have to write cron jobs to manage token expiry again. For a deeper look at this mechanism, read our guide on architecting reliable token refreshes.
Store the integrated_account_id returned from the Link UI against your customer record. Every subsequent API call and webhook event is keyed by this ID. Treat it like a foreign key in your database.
Step 2: Configuring Unified Webhook Subscriptions
Once a customer has connected their account, your application needs to know when data changes in their third-party system. Polling APIs every five minutes is an anti-pattern that leads to severe rate limiting, massive egress costs, and delayed syncs. You need webhooks.
But third-party webhooks are chaotic. Some providers (like Salesforce) require you to programmatically configure webhooks per connected account. Other providers send a massive firehose of events for all customers to a single environment-level URL, requiring you to query your database to fan out the events to the correct tenant context.
A unified webhook subscription collapses all of this architectural divergence into a single API call. You simply register your destination URL and specify the unified event types you care about.
Creating the Subscription
curl -X POST 'https://api.truto.one/webhook' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"target_url": "https://api.yourdomain.com/webhooks/truto",
"is_active": true,
"environment_id": "env_production",
"event_types": [
"record:created",
"record:updated",
"record:deleted",
"integrated_account:authentication_error",
"integrated_account:reactivated"
]
}'That is it. This single API call subscribes your application to data changes across every integration you support. Whether a user connects HubSpot, Salesforce, HiBob, Zendesk, or Pipedrive, you will receive a stream of normalized events arriving at your target_url in the exact same shape.
| Event | Fires when |
|---|---|
record:created |
A new resource is created in the third-party system (employee, contact, ticket, etc.) |
record:updated |
An existing resource is modified |
record:deleted |
A resource is removed |
integrated_account:authentication_error |
A token refresh failed and the account needs reauthentication |
integrated_account:reactivated |
A previously broken account is healthy again |
One subscription handles the fan-out for both webhook architectures. If a provider sends one webhook per account, or if it sends a single environment-level webhook and expects context-lookup-based fan-out, the customer-facing event contract delivered to your endpoint stays identical.
Step 3: The Magic of Webhook Normalization
To truly appreciate the engineering hours saved by this architecture, we must examine what happens between the third-party provider firing an event and your server receiving it. This is the step that pays for the entire architecture.
sequenceDiagram
participant Provider as Third-Party API
participant Ingress as Webhook Router
participant Mapper as JSONata Transformation
participant Enricher as Enrichment Engine
participant Queue as Outbound Queue
participant Customer as Your Application
Provider->>Ingress: POST Raw Event (e.g. employee.joined)
Ingress->>Ingress: Verify Signature (HMAC/JWT/Challenge)
Ingress->>Mapper: Evaluate JSONata Mapping
Mapper-->>Enricher: Mapped Event (Requires Full Payload)
Enricher->>Provider: GET /unified/hris/employees/{id}
Provider-->>Enricher: Full Employee Record
Enricher->>Queue: Enqueue record:created via Claim-Check
Queue->>Customer: POST Standardized Event + X-Truto-Signature1. Verification and Handshakes
Webhook signature verification is a fragmented mess. Slack requires a URL verification challenge before it sends data. Microsoft Graph requires echoing validation tokens. Shopify uses HMAC-SHA256 over the request body, while others use Basic Auth or JWTs in the headers.
The platform handles these cryptographic verifications natively. Each pattern is declared as configuration on the integration. If the payload is a challenge request, the ingress router responds with the correct handshake. If it is a standard payload, it computes the timing-safe hash comparison to prove authenticity. None of this is your problem after the integration is configured once.
2. JSONata Transformation
Once verified, the raw payload is reshaped into a canonical event format. This is done using JSONata—a highly expressive, Turing-complete transformation language—to map provider-specific fields to the unified schema.
For example, HiBob might send an employee.created event with a deeply nested payload, while Workday sends a Worker_Hire XML payload. Declarative JSONata mappings translate both of these into a canonical record:created event for the unified hris/employees resource. This means the platform operates with zero integration-specific code in its backend logic. The mapping language lives as data, not as a massive switch statement in a handler.
3. Data Enrichment and Rate Limits
Many providers send "thin" webhooks. They notify you that an event occurred but only provide the record ID. To be useful, your application needs the actual data.
When a thin payload is detected, the enrichment engine automatically queries the provider's API to fetch the complete, up-to-date unified resource. Your endpoint receives a complete record, not a notification you have to chase with an extra API call.
A critical note on rate limits: If the third-party provider responds to this enrichment call with an HTTP 429 Too Many Requests, the platform does not magically absorb the error or apply silent backoff. Radical transparency is better for distributed systems. The rate limit error is passed downstream, normalizing the provider's varied rate limit headers into standard IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset). Your application must respect these headers and handle its own backoff strategy.
4. Decoupled Storage and Queued Delivery
Webhook delivery is inherently unreliable. Networks partition, servers crash, and databases lock. Delivery is guaranteed through a distributed queueing system with exponential backoff.
To handle massive enterprise payloads without hitting message size limits, the platform utilizes the claim-check architectural pattern. Before an event is enqueued, the large JSON payload is written to highly available object storage. The queue message only contains a lightweight reference ID. When the delivery worker picks up the message, it retrieves the full payload from object storage and dispatches it to your endpoint. If your server is down and returns a 503, the worker acknowledges the failure and schedules a retry. The payload remains safely stored until successful delivery or expiration.
A real trade-off worth naming: on the inbound integrated-account path, top-level errors are intentionally swallowed so the third-party does not retry the same event. Per-event failures are logged, not surfaced as 5xx responses. This prevents storms of retried duplicate events from chatty providers, but it means observability on inbound failures matters. Use webhook health alerts to catch silent drops.
For a deeper architectural read on webhook reliability, see Designing Reliable Webhooks.
Step 4: Processing the Standardized Event in Your App
After normalization and enrichment, the event is ready for delivery. Instead of writing custom parsers for 50 different APIs, your backend only needs one route handler to process unified webhooks.
Verifying the Outbound Signature
All outbound webhooks are signed using HMAC-SHA256. The signature is passed in the X-Truto-Signature header. You must verify this signature to ensure the payload originated from the trusted platform and was not tampered with.
Here is how you verify the signature and process the event in a standard Node.js Express application:
import crypto from 'crypto';
import express from 'express';
const app = express();
// CRITICAL: Use raw body parser to preserve exact payload bytes for hashing
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/truto', async (req, res) => {
const signatureHeader = req.headers['x-truto-signature']; // e.g., format=sha256,v=abc123def456...
const webhookSecret = process.env.TRUTO_WEBHOOK_SECRET;
if (!signatureHeader) return res.status(401).send('Missing signature');
// Extract the hash value from the header
const signatureParts = signatureHeader.split(',');
const hashValue = signatureParts.find(part => part.startsWith('v=')).substring(2);
// Compute the expected HMAC over the raw bytes
const expectedHash = crypto
.createHmac('sha256', webhookSecret)
.update(req.body)
.digest('base64');
// Use timingSafeEqual to prevent side-channel timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(hashValue),
Buffer.from(expectedHash)
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Parse the verified JSON and process the event
const event = JSON.parse(req.body.toString());
try {
switch (event.event) {
case 'record:created':
case 'record:updated':
await upsertRecord({
tenantId: event.payload.integrated_account_id,
resource: event.payload.resource,
records: event.payload.records,
});
break;
case 'record:deleted':
await deleteRecord(event.payload);
break;
case 'integrated_account:authentication_error':
await flagAccountForReauth(event.payload.integrated_account_id);
break;
}
return res.status(200).send('OK');
} catch (error) {
console.error('Failed to process webhook:', error);
// Return 500 to trigger exponential backoff retry
return res.status(500).send('Internal Server Error');
}
});The shape of event.payload.records is the same across every provider for a given unified resource. A crm/contacts event always has first_name, last_name, email_addresses, and phone_numbers. You write one handler per unified resource, not one handler per provider.
Reliability and Idempotency
Treat the handler as idempotent. Webhook delivery is at-least-once, and distributed queueing systems make no strict ordering guarantees across retries and concurrent processing. Use the unique event ID for deduplication if your downstream system is sensitive to repeats, and always architect your database operations to upsert (e.g., ON CONFLICT DO UPDATE in PostgreSQL) rather than blindly insert.
Why This Architecture Scales Your Roadmap
The quickstart is not just about saving time on day one. It is about what does not happen on day 180. Building integrations in-house is a trap. You start by writing a simple OAuth redirect for HubSpot. Six months later, you have three engineers dedicated entirely to maintaining token refresh scripts, parsing undocumented API errors, and rewriting webhook handlers because Salesforce changed their payload structure.
By adopting a unified architecture, you gain three compounding effects:
- No integration-specific code in your repository. Your single webhook handler is the entire surface area, regardless of whether you support 5 or 50 providers.
- Token refreshes happen ahead of expiry. Webhooks never fail because an access token went stale at the wrong moment.
- Failures are visible. Authentication errors become events. Persistent delivery failures auto-disable the subscription and notify you. Silent breakage—the actual killer of customer-facing integrations—is drastically reduced.
The honest trade-off: a unified abstraction is always a lossy one. If a provider exposes a highly niche field or a non-standard event type, a strict unified schema may not cover it. However, the escape hatches are real. The remote_data object preserves the full, raw third-party payload on every event, custom resources let you expose endpoints outside the unified model, and per-environment overrides let you extend mappings without forking. But if your core product depends entirely on a single provider's idiosyncrasies, a unified API might be the wrong tool. For broad, standardized coverage across a category, it is the only sane tool.
Next Steps
If you are evaluating this architecture for a real roadmap, the practical next steps are straightforward:
- Pick one customer-facing integration you have been deferring (Salesforce, HubSpot, or BambooHR are common starting points).
- Implement the four steps above against a sandbox account in an afternoon.
- Compare the time-to-ship against your engineering team's current estimate for the in-house build.
The gap is usually the difference between shipping this quarter and pushing to next year. Elite engineering teams do not waste cycles building undifferentiated plumbing for third-party APIs. They drop in a unified abstraction layer, say "yes" to enterprise sales requests, and focus their engineering bandwidth on features that actually move the needle.
FAQ
- What is the difference between a unified webhook and a regular webhook?
- A regular webhook is a raw HTTP callback from a single provider with that provider's custom payload shape and signature scheme. A unified webhook is a normalized event delivered after verification, JSONata-based mapping, and optional enrichment—so a HubSpot contact creation and a Salesforce contact creation arrive at your endpoint as the exact same `record:created` shape.
- How does the Link UI handle OAuth token refresh?
- The Link UI captures the initial OAuth grant and hands off to a backend that proactively refreshes access tokens shortly before they expire, using a safety buffer. Concurrent refresh attempts are serialized to prevent stampeding the provider. If a refresh fails permanently, an `integrated_account:authentication_error` event fires so your app can prompt the user to reauthorize.
- Do I need to handle different signature schemes for each provider?
- No. Inbound provider signatures (HMAC, JWT, Basic, Bearer) and challenge handshakes are verified by the unified webhook engine using declarative configuration. Your endpoint only needs to verify a single outbound `X-Truto-Signature` header using HMAC-SHA256.
- How are thin webhooks (ID-only payloads) handled?
- When a provider sends a thin webhook containing just a resource ID, the enrichment engine automatically calls the provider's API to fetch the full record, maps it to the unified schema, and delivers a complete payload to your endpoint. You do not have to chase the API yourself.
- What happens if my webhook endpoint is down or payloads are too large?
- Outbound delivery uses queue-based retry with exponential backoff. To bypass message size limits, the platform uses a claim-check pattern, storing the raw payload in object storage and passing a lightweight reference through the delivery queue. Retries always have the full data available.