Integrations - SMB Sales Boost API
SMB Sales Boost can send lead data to your CRM, automation tools, and any HTTPS endpoint. Set up destinations from the Integrations tab in your dashboard, or with the endpoints below, which are also available as MCP tools (pushing your own lead data is REST only; see MCP Server). They live under /api/v1/integrations, use the same API key (Authorization: Bearer smbk_...), paid-plan requirement and rate limits as the rest of the API, and return the usual {"data": ...} and {"error", "message"} shapes. A 400 validation_error can also include a details array (see Error Handling).
Safeguards
An integration sends business contact details to an outside system automatically, on every subscribed event, until it is paused or deleted. So that nothing changes unnoticed:
- When an API key or MCP client creates a webhook, connects a new CRM account, changes where an integration sends data, adds or re-activates an event, or re-enables an integration (including a successful test or retry, or a reconnect, that turns a needs_attention integration back on), the account owner is emailed for each change. Each account gets up to 10 of these emails an hour for destination changes and 10 for other changes; past that, one alert is sent and further changes are combined into summary emails.
- The dashboard shows which API key created or last changed each integration, for example "Created via API key smbk_AbCdEfG..." or, when the change came through the MCP server, "Created via MCP with API key smbk_AbCdEfG...". A change in the dashboard replaces that record only when it adds a destination URL the integration never had. Pausing an integration (also when you revoke its key), and resuming it, switching on more events or switching on a URL it already had in the dashboard, keep it.
- When you revoke an API key in Dashboard > API, you can also pause every integration that key created or last changed (off by default). Paused integrations can be re-enabled in the Integrations tab or with
PATCH /api/v1/integrations/:idand{"status": "connected"}. A CRM connect link created with a key that is later revoked stops working: nothing is connected or changed. - The webhook signing secret is returned only once, when the webhook is created.
- A header API key stored on a webhook (sent as
X-SMB-API-Key) is only sent to the active destination URLs it was set for: it is bound to them when it is set, and editing the integration in the dashboard binds it again to the integration's active destinations on the same origin. A URL on a switched-off subscription is never bound, so switching that subscription on later does not send the key there until the owner edits the integration in the dashboard orapiKeyis sent again. Pointing such a webhook at a new destination URL with an API key requiresapiKeyin the same request (a new value, ornullto stop sending it), even when the new URL is on the same host: services like Zapier, Make and Clay put every customer's hooks on one host.
Supported providers
- HubSpot (
hubspot) - Per-user OAuth. Creates or updates Contacts matched by email (a lead without an email creates no Contact) and Companies matched by website domain, or by exact company name when the lead has no website of its own (none, or a social or listing page such as Facebook or Yelp), and links each Contact to its Company (available when configured)
- Salesforce (
salesforce) - Per-user OAuth, Production or Sandbox. Creates or updates Leads matched by Email; a lead without an email creates a new Lead each time, and Company defaults to "Unknown" when the lead has no company name (available when configured)
- Pipedrive (
pipedrive) - Per-user OAuth; per-company API domain handled automatically. Matches Organizations by exact name and Persons by the SMB Sales Boost lead id, then by email; adds a note with the lead id on each delivery of a lead; optional Deals, see Rules worth knowing (available when configured)
- Zapier (
zapier) - Webhook trigger URL with HMAC signature (Live)
- n8n (
n8n) - Webhook node with HMAC signature (Live)
- Make.com (
make) - Custom webhook trigger with HMAC signature (Live)
- Pipedream (
pipedream) - HTTP source workflow with HMAC signature (Live)
- Clay (
clay) - Clay table "Import data" webhook with HMAC-signed JSON rows (Live)
- Generic Webhook (
generic_webhook) - Any HTTPS endpoint, signed JSON payload (Live)
HubSpot, Salesforce and Pipedrive are available when they are configured for this deployment; otherwise connecting them, or using an already connected one, returns 503 provider_unavailable. GET /api/v1/integrations/providers shows the current availability of every provider.
Events
Subscribe destinations to one or more of these events. A webhook receives each event as a signed JSON POST (see Delivery format below).
lead.created- one event per lead, the first time a lead reaches your account (dashboard or API export, API search, email schedule, lead reservoir or push), with that lead's business contact details. Leads you get through a URL enrichment run send nolead.createdorlead.updated; the next time such a lead reaches your account it sendslead.updated.lead.updated- one event per lead when a lead you already received reaches your account again with a newer Last Buying Signal than when you last received it (an export or email schedule that includes it, a push to another integration, or aGET /api/v1/leadspage or MCPsearch_leadscall that returns it), or when a Lead Export History refresh finds that newer data. Receiving an unchanged lead again sends nothing. The push target itself receives the push aslead.created(see Push one lead), a refresh sends nothing for a lead on your export blacklist, andGET /api/v1/leads/previewand re-exports from Lead Export History that do not refresh send nothing. Pipedrive integrations subscribe to it by default (HubSpot has it off).export.completed- a lead reservoir export finished.data:count,databaseType,sourceandsampleLeads(up to 5 leads, each withid,companyName,website,email,phone,cityandstate).email_schedule.sent- a scheduled lead email was sent.data:scheduleId,scheduleName,leadCount,creditableLeadCountandsampleLeads(up to 5 leads, each withid,companyName,website,email,phone,cityandstate).credit.low_balance- sent after each credit charge while your total remaining credits (monthly, permanent and temporary) are at or below the larger of 50 and 10% of your monthly credit allowance, so it can arrive many times; accounts without a monthly allowance never get it.data:remainingMonthly,remainingPermanent,remainingTemporary,remainingTotal,monthlyAllowance,threshold.
HubSpot, Salesforce and Pipedrive integrations take only lead.created and lead.updated; a subscription to any other event returns 400 validation_error.
Delivery format
Each webhook delivery is an HTTPS POST with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | SMB-Sales-Boost-Webhooks/1.0 |
X-SMB-Event | The event type, for example lead.created |
X-SMB-Timestamp | Unix seconds when this attempt was signed |
X-SMB-Signature | t=<Unix seconds>,v1=<hex> (see Verifying signatures) |
X-SMB-API-Key | Only when the webhook has a header API key set for this destination URL (see Safeguards) |
The body is an envelope; data depends on the event (shortened here):
{
"id": "evt_1760000000000_k3j9qz",
"type": "lead.created",
"occurredAt": "2026-10-08T12:00:00.000Z",
"userId": "your-user-id",
"integrationId": 42,
"provider": "zapier",
"data": {
"id": 123456,
"databaseType": "other",
"source": "api",
"companyName": "Acme Plumbing",
"website": "https://acmeplumbing.example",
"email": "[email protected]",
"phone": "(512) 555-0100",
"city": "Austin",
"state": "TX",
"lastBuyingSignal": "2026-10-01T00:00:00.000Z",
"buyingSignalType": ["Newly Added"]
}
}
| Delivery | data |
|---|---|
Live lead.created / lead.updated, and a leadId push (lead.created) | A flat lead summary: id, databaseType, source (dashboard, api, email_schedule or crm_push; a push has crm_push), companyName, website, email, emailSecondary, emailTier1 to emailTier6, phone, phoneSecondary, phoneTotal, address, city, state, zip, country, category, description, lastBuyingSignal and buyingSignalType (a list). lastUpdated and lastUpdatedReason carry the same two values under their older names. |
Push of your own data (lead.created) | {"lead": {...}, "source": "manual_push"} with your own object as sent (see Push one lead) |
Test (lead.created) | {"test": true, "message": "This is a test event from SMB Sales Boost integrations", "sample": {"id": 0, "companyName": "Acme Test Co", "website": "https://example.com"}} |
export.completed, email_schedule.sent, credit.low_balance | The fields listed under Events |
A receiver that handles pushes and tests must read three lead.created shapes: the flat lead summary (live events and leadId pushes), your own data from a push, and the test event.
- Answer with a 2xx status within 8 seconds. Anything else is a failed attempt: a 3xx (redirects are not followed), a 4xx, a 5xx, a timeout or a connection error.
- A live webhook event is tried up to 5 times, waiting about 1, 2, 4 and 8 seconds (plus up to 1 second) between attempts. Every attempt sends the same body and
idwith a newX-SMB-Timestampand signature, so useidto ignore duplicates. The delivery appears inGET /api/v1/integrations/:id/deliveriesonce its last attempt has finished. - Tests, pushes, manual retries and deliveries to HubSpot, Salesforce and Pipedrive are one attempt each. A test's
idstarts withevt_test_; a manual retry sends a newid(evt_retry_...) withoccurredAtset to the time of the retry. - Each lead is its own request, and webhook requests are sent in parallel: a large export can send many at once, and events arrive in no particular order. Deliveries to HubSpot, Salesforce and Pipedrive are sent at most 3 at a time per integration, and HubSpot calls are paced to HubSpot's rate limits, so a large export reaches the CRM gradually. Pausing a CRM integration, or switching an event off, also stops the lead deliveries still waiting to be sent to it.
Verifying signatures
X-SMB-Signature is t=<Unix seconds>,v1=<hex>. t is the same value as X-SMB-Timestamp, and v1 is the hex HMAC-SHA256 of "<t>." + rawBody, keyed with your signing secret exactly as it was returned (use the string as is; do not hex-decode it). Check it against the raw request body before you parse the JSON, compare using a timing-safe comparison after checking that the lengths match, and reject a t older than 5 minutes (every attempt is signed with a fresh t).
// Node.js (Express): verify an SMB Sales Boost webhook before using it
const crypto = require("crypto");
const express = require("express");
const app = express();
const SIGNING_SECRET = process.env.SMB_SIGNING_SECRET; // exactly as returned on create
const MAX_AGE_SECONDS = 5 * 60;
// Mount this route before app.use(express.json()), which would consume the raw body.
app.post("/smb-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!Buffer.isBuffer(req.body)) return res.status(400).send("Expected a JSON body");
// X-SMB-Signature: t=<Unix seconds>,v1=<hex>
const parts = {};
for (const item of (req.get("X-SMB-Signature") || "").split(",")) {
const i = item.indexOf("=");
if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return res.status(400).send("Missing signature");
if (Math.abs(Date.now() / 1000 - t) > MAX_AGE_SECONDS) return res.status(400).send("Stale timestamp");
// HMAC-SHA256 of "<t>." followed by the raw body bytes, keyed with the secret string
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(parts.t + ".")
.update(req.body)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("Bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // answer within 8 seconds; do slow work afterwards
console.log(event.type, event.id); // use event.id to skip duplicates
});
app.listen(3000);
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/integrations/providers | List providers and runtime availability |
GET | /api/v1/integrations | List your integrations with subscriptions and who created or last changed them |
GET | /api/v1/integrations/:id | Single integration with subscription detail |
POST | /api/v1/integrations/webhook | Create a webhook integration; returns the signing secret once |
PATCH | /api/v1/integrations/:id | Update display name, status, subscriptions (the list you send replaces them all), destination URL, header API key or Pipedrive deal settings |
DELETE | /api/v1/integrations/:id | Delete an integration permanently, with its subscriptions and delivery history |
GET | /api/v1/integrations/:id/deliveries | The 50 newest deliveries, each with its attempt count, last status code, error and the first 1000 characters of your endpoint's response |
POST | /api/v1/integrations/:id/deliveries/:deliveryId/retry | Send the stored data of a past delivery again as a new event, in one attempt |
POST | /api/v1/integrations/:id/test | Send one test lead.created event to the real destination (on a CRM this writes a real record for the sample company Acme Test Co that is not removed) |
POST | /api/v1/integrations/:id/push-lead | Push one lead now: leadId (1 credit if new to you) or your own lead data (free) |
POST | /api/v1/integrations/:provider/connect | Start a HubSpot, Salesforce or Pipedrive connection, or reconnect one already connected; returns authorizationUrl |
GET | /api/v1/integrations/:id/field-mapping | CRM field list and current mapping (?refresh=true reloads the CRM fields) |
PATCH | /api/v1/integrations/:id/field-mapping | Change which CRM field receives each lead field (replaces the whole saved mapping) |
DELETE | /api/v1/integrations/:id/field-mapping | Reset the mapping to the defaults |
POST | /api/v1/integrations/:id/field-mapping/test | Dry-run a mapping (creates, then deletes, synthetic CRM records) |
GET | /api/v1/integrations/:id/pipedrive/pipelines | Pipedrive pipelines and stages |
GET | /api/v1/integrations/:id/pipedrive/deal-fields | Pipedrive deal fields and the deal field map (?refresh=true reloads them) |
Errors: 400 validation_error or bad_request, 402 insufficient_credits (push-lead), 404 not_found, 409 integration_exists (create, or a PATCH that re-points to a destination another integration of the same provider already uses) or integration_limit (at most 25 integrations per account; also on connect, see Connect a CRM), 409 conflict (PATCH only: when the integration changes while the update is being applied, for example the owner pauses it at the same moment, the update is applied again on top of that change; only when it changes again does the PATCH return 409, nothing changed, so read it again and retry. If the API key making the PATCH is revoked in that moment, it returns 401 unauthorized "This API key has been revoked" and nothing is applied), 429 (all integration endpoints share one budget per account of 60 requests per minute, counted together with the dashboard; connect is also limited to 30 per minute), 502 provider_error (the CRM's API failed) and 503 provider_unavailable. A 503 provider_unavailable is not temporary (no Retry-After; do not retry): it means the CRM is not configured for this deployment. It can come from the field-mapping, Pipedrive pipeline and deal-field endpoints, test, retry, push-lead and connect. An unknown provider in the connect path is a 404. A 500 internal_error on create, PATCH or push-lead may come after the change was saved (or a credit was taken), so check GET /api/v1/integrations and the deliveries before repeating it; a 503 service_unavailable only happens before anything is written.
Responses
Every success is wrapped in {"data": ...}. Test, retry and push answer HTTP 200 even when the destination fails; read data.status. When a webhook test or push gets a non-2xx answer, data.error holds the first 500 characters of your endpoint's response (HTTP <status> when it sent no body). Paths starting with .../ are under /api/v1/integrations/:id/.
| Endpoint | data |
|---|---|
GET /api/v1/integrations/providers | A list of {id, name, tagline, description, authMethod, ownerSetupRequired, defaultEvents, supportsPerLeadPush, helpText, available}, plus setupHelpUrl when set and reason when unavailable |
GET /api/v1/integrations (a list), GET and PATCH /api/v1/integrations/:id | An integration: {id, provider, providerName, status, displayName, externalAccountId, authMethod, metadata, consecutiveFailures, lastFailureAt, lastSuccessAt, tokenExpiresAt, createdAt, updatedAt, createdVia, lastChangedVia, lastChangedAt, subscriptions: [{id, eventType, targetUrl, isActive, config}]}. status is connected, disabled or needs_attention. createdVia and lastChangedVia are null or, for example, {"channel": "api", "apiKey": {"id": 7, "keyPrefix": "smbk_AbCdEfG...", "name": "Zap agent", "revoked": false}}; channel is dashboard, api or mcp, and apiKey is null for a dashboard change. |
POST /api/v1/integrations/webhook | {"integration": {...}, "signingSecret": "..."} (see Create a webhook integration) |
DELETE /api/v1/integrations/:id | {"message": "Integration deleted"} |
GET .../deliveries | Newest first, a list of {id, eventType, targetUrl, status, statusCode, attemptCount, latencyMs, errorMessage, responseBody, createdAt, completedAt}. CRM rows have a targetUrl such as hubspot://contacts. |
POST .../test, POST .../deliveries/:deliveryId/retry | {"status": "success" or "failed", "statusCode", "latencyMs", "error"}: statusCode and latencyMs come from webhooks, and error, when present, explains the failure |
POST .../push-lead | {"status", "tracked", "creditsUsed", "result", "error"}: result is {contactId, companyId} for HubSpot (each only when written), {leadId} for Salesforce and {statusCode, latencyMs} for a webhook (none for Pipedrive) |
GET .../field-mapping | HubSpot {smbFields, contactFields, companyFields, cachedAt, mapping}; Salesforce {smbFields, leadFields, contactFields, accountFields, cachedAt, mapping} (only the lead scope is mapped); Pipedrive {smbFields, personFields, organizationFields, cachedAt, mapping}. CRM field lists are cached for up to 10 minutes. |
PATCH and DELETE .../field-mapping | {"mapping": {...}}, the mapping now saved (after DELETE every scope is empty, which means the defaults) |
POST .../field-mapping/test | {ok, scopes: [{scope, label, ok, issues: [{smbFieldId, targetField, message, severity}]}]}, where severity is error or warning |
GET .../pipedrive/pipelines | A list of {id, name, active, stages: [{id, name, orderNr}]} |
GET .../pipedrive/deal-fields | {smbFields, dealFields, cachedAt, fieldMap} |
Rules worth knowing
subscriptionson PATCH replaces the integration's whole subscription set, so send the complete list you want (at most 20 items). Each item is{"eventType": "lead.created", "targetUrl": "https://...", "isActive": true, "config": {...}}: onlyeventTypeis required,isActivedefaults totrueandconfigto null. An event you leave out is removed and stops being sent, an empty list removes them all, and of two items with the same event and URL only the first is kept. Subscription ids change wheneversubscriptionsordefaultTargetUrlis sent. To add or switch on one event, read the integration first (GET /api/v1/integrations/:id) and send back every subscription you want to keep, withisActive: falseon the ones that should stay off. For example, to switch on HubSpotlead.updated, send{"subscriptions": [{"eventType": "lead.created"}, {"eventType": "lead.updated"}]}. On a HubSpot, Salesforce or Pipedrive integration, an item with any other event returns400 validation_errorand nothing is saved (see Events).defaultTargetUrlis for webhook integrations only (400 on a CRM). Sent withoutsubscriptions, it re-points every existing subscription, inactive ones too. Sent withsubscriptions, it fills in each subscription that has notargetUrl. A subscription with neither keeps that event's current URL, or takes the integration's first URL for a new event.statuson PATCH takesconnectedordisabled.disabledpauses all sending;connectedre-enables a disabled orneeds_attentionintegration and resets its failure count.needs_attentionis set only by SMB Sales Boost, after 5 failed deliveries in a row. One failed delivery is, for example, a live webhook event whose 5 attempts all failed, a failed CRM write, a failed push, a retry that could not reach the destination, or a test the destination rejected. A successful live delivery, test, retry or push resets the count, and a disabled integration never changes toneeds_attention. Sendingneeds_attentionreturns 400 (unless the integration already has that status). Events that happen while an integration is disabled orneeds_attentionare not queued and are never sent later.GET /api/v1/integrations/:idshowsconsecutiveFailures,lastFailureAtandlastSuccessAt.- Live events never use an inactive subscription, and webhook tests and pushes do not either: they use the active subscription for the event, else any active subscription with a URL; with none they fail with "No active subscription for this integration" (a CRM test or push goes to the connected CRM). A new URL counts as a destination change (owner email) even on an inactive subscription, and so does switching on a subscription whose URL was not active before.
apiKeyon PATCH applies only to Clay integrations and to webhook integrations that already send a header API key (400 otherwise, and on a CRM). A new value replaces the key;nullor an empty string stops sending it. Pointing such an integration at a new destination URL (any URL it does not already use, even on the same host) through the API or MCP requiresapiKeyin the same request, or the PATCH returns 400 and nothing changes. The integration and its subscriptions are saved together; a500may come after the change was saved, so checkGET /api/v1/integrations/:idbefore repeating a PATCH.- Test and retry never charge credits and never add a lead to your Lead Export History, even when the retried delivery was a push; both answer 200 with
data.status(see Responses).POST /api/v1/integrations/:id/deliveries/:deliveryId/retrytakes the numericidfromGET .../deliveries(not theevt_id in the webhook body) and re-sends that delivery's stored data, to the URL it was first sent to or to the connected CRM, as a new event: a newid,occurredAtset to the time of the retry, and one attempt. A test or retry that is sent is recorded as a new delivery (the original stays), and one that fails counts towardneeds_attention, whether the destination rejected it or could not be reached (for example a timeout). On a CRM, a test writes a real record for the sample company Acme Test Co that is not removed;POST .../field-mapping/testcleans up after itself. On a paused integration, test and retry return 400 ("Integration is paused. Re-enable it first (PATCH status connected)."); a successful test or retry on aneeds_attentionintegration turns it back toconnected, which counts as re-enabling it. - Field mapping: each CRM takes its own scopes (HubSpot
contactandcompany, Salesforcelead, Pipedrivepersonandorganization). Each scope maps an SMB field id (the ids insmbFields) to the CRM field's internal name, for example{"contact": {"phone": "mobilephone"}}. PATCH replaces the whole saved mapping: any field or scope of this CRM that you leave out goes back to its default, so read the mapping first and send every override you want to keep. Unknown SMB field ids are dropped (the response shows what was saved). A scope of another CRM, or a PATCH with no scope at all, returns 400validation_errorand changes nothing; use DELETE to reset the mapping.POST .../field-mapping/testtakes an optional{"mapping": {...}}to test a mapping before saving it; without it, the saved mapping is tested. The field-mapping endpoints return 400 on a webhook integration, and the Pipedrive endpoints on any other provider. pipedriveDealson PATCH (Pipedrive only, 400 otherwise) turns on Deal creation:{"enabled": true, "pipelineId": 1, "stageId": 2, "defaultValue": 0, "defaultCurrency": "USD", "expectedCloseDays": 30, "fieldMap": {}}, wheredefaultValueis 0 or more,defaultCurrency3 to 8 characters,expectedCloseDays0 to 3650, andfieldMapmaps an SMB field id to a deal field key fromGET .../pipedrive/deal-fields. Fields you send are merged into the saved settings; afieldMapyou send replaces the saved one.enabled: trueneedspipelineIdandstageIdin the same request, even when they are already saved (take them fromGET .../pipedrive/pipelines). For each lead, an open Deal already linked to the lead, or else an open Deal of the Person, is updated; otherwise a new Deal is created (a won or lost Deal is never reused).- DELETE is permanent: the integration's subscriptions, field mapping, Pipedrive deal settings and delivery history are removed and cannot be restored. Records already sent stay in the destination, and deleting a CRM integration does not remove SMB Sales Boost's access inside the CRM (revoke it there if needed). A webhook created again gets a new id and signing secret, and a CRM must be connected again in a browser. To stop sending without losing anything, PATCH
{"status": "disabled"}instead.
Create a webhook integration
curl -X POST https://smbsalesboost.com/api/v1/integrations/webhook \
-H "Content-Type: application/json" \
-H "Authorization: Bearer smbk_your_api_key_here" \
-d '{
"provider": "zapier",
"displayName": "My Zap",
"targetUrl": "https://hooks.zapier.com/hooks/catch/XXX/YYY",
"events": ["lead.created", "export.completed"]
}'
Body fields:
provider(required):zapier,n8n,make,pipedream,clayorgeneric_webhook. CRMs are connected withPOST /api/v1/integrations/<crm>/connectinstead (see Connect a CRM).targetUrl(required): a public https URL, up to 2048 characters; localhost and private network addresses return 400.events(required): one or more event names (duplicates are ignored; at most 20 entries).displayName(optional): 1 to 100 characters; defaults to the provider name.apiKey(optional, any webhook provider, up to 500 characters): a secret sent as theX-SMB-API-Keyheader only to thistargetUrl, for example a Clay table API key. It is stored encrypted and never returned. Set it here: a later PATCH acceptsapiKeyonly for Clay and for webhooks that already send one.
The response is {"data": {"integration": {...}, "signingSecret": "..."}}. Store the signing secret now, because it is shown only once. If the request times out, the integration may have been created: check GET /api/v1/integrations first. Repeating the same provider and targetUrl returns 409 integration_exists; a different provider or a changed URL (even a trailing slash) creates a second integration with its own secret. If the integration exists but you did not get its signing secret, delete it (DELETE /api/v1/integrations/:id, which also deletes its delivery history) and create it again.
Connect a CRM
POST /api/v1/integrations/hubspot/connect (or salesforce, pipedrive) returns {"data": {"provider", "authorizationUrl", "expiresAt"}}. Salesforce also takes {"environment": "production"} (the default) or {"environment": "sandbox"}; sending environment for HubSpot or Pipedrive returns 400. Open authorizationUrl in a browser and approve access within 10 minutes. A new CRM integration then appears in GET /api/v1/integrations with its initial events: HubSpot lead.created on (lead.updated present but off; to turn it on, PATCH subscriptions with both events, see Rules worth knowing), Salesforce lead.created, Pipedrive lead.created and lead.updated. Only the account owner should approve it: whoever approves attaches their CRM to the account. When the account already has 25 integrations, connect returns 409 integration_limit unless one of them is an integration of that CRM that the sign-in could reconnect. The browser returns through /api/integrations/<provider>/callback, which is a redirect target, not an API endpoint.
Connecting a CRM account that is already connected re-authorizes that integration in place: it gets new access, a needs_attention integration becomes connected again, a paused one stays paused, and its events, field mapping, Pipedrive deal settings and delivery history are kept. The 25-integration limit applies only to new connections. So when a CRM integration stops working because its access was removed or expired in the CRM, reconnect it: click Reconnect on the integration in the dashboard's Integrations tab, or connect again through the API or MCP and sign in to the same CRM account (for a Salesforce Sandbox integration, send {"environment": "sandbox"}). PATCH {"status": "connected"} alone does not restore access. Signing in to a different account of the same CRM creates a new integration.
If approval does not complete, nothing is connected or changed and the dashboard URL shows <provider>_error=<reason>, for example key_revoked (the API key that created the link was revoked), integration_limit (the account has 25 integrations and the CRM account you approved is not already connected), invalid_state (the link expired or was already used; start again), already_connected (rare: two approvals of the same CRM account finished at the same moment) or the CRM's own error, such as access_denied. A HubSpot connect fails, creating nothing, when HubSpot does not confirm the account, with the message "We could not confirm which HubSpot account you signed in to, so nothing was connected or changed. Please try connecting again in a minute." Check GET /api/v1/integrations to confirm a connection.
Push one lead
POST /api/v1/integrations/:id/push-lead sends one lead to this integration now, in a single attempt with no automatic retries. Send {"leadId": 123} for a lead from the database (tracked like an export), or {"lead": {...}} for your own data (untracked and free; REST only, the MCP tool push_lead_to_integration takes leadId).
- Cost: A lead you already received is free, even at a 0 balance. A new lead costs 1 credit, taken from your balance or, when the balance is 0 and your overage budget is enabled with room left, billed as overage; if neither can cover it, the push returns 402 insufficient_credits and nothing is pushed. A new lead that shares a primary phone or primary email with a lead you already received is also free (see Credits & Subscription). The credit is taken before the lead is sent: if the destination then fails, the response is a 200 with status
failedandtracked: true, the credit stays used, and because the lead now counts as received, pushing the sameleadIdagain is free. - Refused before anything is charged or sent: an integration whose status is not
connected(a disabled orneeds_attentionintegration returns 400 until you PATCH{"status": "connected"}; unlike test and retry, push does not work onneeds_attention), aleadIdthat matches your export blacklist (400), an unknownleadId(404),leadIdandleadsent together (400), and a webhook with no active subscription (a 200 with statusfailed,tracked: falseandcreditsUsed: 0). Your own data is not checked against the blacklist. - Your own data: use the keys
email,firstName,lastName,phone,companyName,website,industry,cityandstateso CRMs can map it (the database field names, such aswCompanyNameandwEmailPrimary, also work: they are the names Lead Export HistoryleadSnapshotuses, not the display namesGET /api/v1/leadsreturns, such asCompany Name). CRMs match and create records as described under Supported providers; for example HubSpot writes a Contact only when there is an email. - What arrives: a webhook gets one
lead.createdevent, even for a lead you received before. AleadIdpush to a webhook sends the same flat lead summary as a livelead.createdevent, withsourcecrm_push. A push of your own data sends{"lead": <your object as sent>, "source": "manual_push"}; see Delivery format. For aleadIdpush, the target integration gets the lead once, from the push itself; it is left out of the backgroundlead.created/lead.updatedevents. Your other integrations still get them: a lead you had not received before sendslead.createdto your other integrations subscribed tolead.created, and re-pushing a lead you already received sendslead.updatedto your other integrations subscribed tolead.updatedonly when the lead changed since you last received it (a newer Last Buying Signal). A push of your own data goes only to the target integration. - Where it is recorded: every push is recorded in the integration's deliveries and counts toward
needs_attentionlike any other delivery. That covers every push that is sent, through the API, MCP or the dashboard (its per-lead push, or a push of selected leads from the Leads tab, where each lead counts as one delivery), to any provider, HubSpot and Salesforce included; seeGET /api/v1/integrations/:id/deliveries. AleadIdpush that passes the checks is recorded in Lead Export History withexportMethodcrm_push, even if the delivery then fails, and adds 1 tototalLeadsExportedandmonthlyLeadsExportedinGET /api/v1/me(a free re-push too);exportMethodshows the most recent channel, so a later search or export of the lead changes it. A push never creates a file in Export History, and a push refused before anything is sent is not charged and not recorded anywhere. - Errors: a
503 service_unavailableonly happens before anything is charged or sent, so it is safe to retry. A500may come after a credit was taken: checkGET /api/v1/meand the deliveries before pushing the same lead again. The response shape is under Responses.
API Documentation Sections
Introduction | Programmatic Purchase | Authentication | Rate Limits | User Profile | Enrichments | Leads | Filter Presets | Keyword Lists | Email Schedules | Export Formats | Export History | Lead Export History | Account Settings | AI Features | Export Blacklist | Error Handling | Credits & Subscription | MCP Server | Integrations
Ready to Find Your Next Customers? Get access to growing local business leads updated every 5 minutes. See plans starting at $49/mo →