Reseller integration
Partner quickstart
For wholesale partners syncing multiple end-customer CRMs into FundingScout. By the end of this page, you'll have funding-event signals flowing into your platform — tagged per end-customer so you can route them downstream.
Building for your own team's pipeline instead? Use the standard 5-minute quickstart — it's simpler.
The architecture in 30 seconds
You don't need to give us access to your end-customers' CRMs. Instead:
- Once a day, push each end-customer's pipeline (the companies they care about) to us via
POST /accounts. Tag each row with their ID. - We continuously monitor funding rounds (press releases, SEC filings, RSS feeds).
- When a funded company matches one of your customers' pipeline rows, we POST a webhook to your endpoint within ~60 seconds. The payload includes the end-customer tag so you know who to route it to.
That's it. The match engine runs entirely on our side. You only deal with two surfaces: push your CRM snapshot to us, receive webhooks back.
Prerequisites
- An API key from Settings → API Keys — when you create the key you'll see both an
fs_live_...token AND anfs_whsec_...webhook signing secret. Save both. Neither is shown again. - An HTTPS endpoint on your server that will receive POST payloads. We'll set its URL via API in step 3.
Step 1 — Sync your customers' pipelines
Each row in the batch carries a metadata.partner_customer_id tag identifying which of your end-customers owns it. Use whatever ID format you already have (UUID, your CRM's internal ID, an email, anything). It comes back verbatim in every match webhook so you can route to the right customer.
curl -X POST https://fundingscout.io/api/v1/accounts \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{
"accounts": [
{
"external_id": "vc42-acme",
"name": "Acme Corp",
"domain": "acme.com",
"metadata": { "partner_customer_id": "vc_42" }
},
{
"external_id": "vc42-vellum",
"name": "Vellum AI",
"domain": "vellum.ai",
"metadata": { "partner_customer_id": "vc_42" }
},
{
"external_id": "vc43-stripe",
"name": "Stripe",
"domain": "stripe.com",
"metadata": { "partner_customer_id": "vc_43" }
}
]
}'
# Response: { "upserted": 3, "errors": [] }Batch up to 1,000 accounts per call. Mix end-customers in the same batch — the metadata tag handles routing. Idempotent on external_id: send the same row again with updated fields, and we'll update the existing record instead of creating a duplicate.
Step 2 — Register your webhook receiver
curl -X POST https://fundingscout.io/api/v1/webhooks \
-H "Authorization: Bearer $FS_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.example.com/hooks/fundingscout"}'
# Response: { "webhook_url": "https://yourapp.example.com/hooks/fundingscout" }Step 3 — Receive + verify match webhooks
When we detect a match, we POST this to your URL:
POST https://yourapp.example.com/hooks/fundingscout
Content-Type: application/json
X-FundingScout-Signature: sha256=<hex>
{
"event": "funding_match",
"match_id": "f1b2c3d4-...",
"match_type": "account_domain",
"matched": {
"account_external_id": "vc42-vellum",
"account_metadata": { "partner_customer_id": "vc_42" },
"contact_external_id": null,
"contact_metadata": null
},
"funding_round": {
"id": "9e34cb64-...",
"company_name": "Vellum AI",
"amount_usd": 20000000,
"funding_type": "series-a",
"website": "https://vellum.ai",
"article_url": "https://www.businesswire.com/...",
"published_date": "2026-05-12",
"ceo_name": "Akash Sharma",
"industry": "AI Infrastructure",
"investors": ["Spark Capital", "Sequoia"],
"confidence_score": 0.95
},
"timestamp": "2026-05-12T01:54:00Z"
}Always verify the signature. The X-FundingScout-Signature header is sha256=<hex> where the hex is HMAC-SHA256(your_webhook_secret, raw_request_body). Verifying confirms the request is from us and the body wasn't tampered with.
Node.js / Express receiver
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const WEBHOOK_SECRET = process.env.FS_WEBHOOK_SECRET // fs_whsec_...
// IMPORTANT: capture the RAW body — JSON.parse() loses whitespace
// which invalidates the signature.
app.post('/hooks/fundingscout',
express.raw({ type: 'application/json' }),
(req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
const got = req.header('X-FundingScout-Signature') || ''
if (
got.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))
) {
return res.status(401).send('invalid signature')
}
const payload = JSON.parse(req.body.toString('utf8'))
const customerId = payload.matched.account_metadata?.partner_customer_id
// Route to the right end-customer's downstream pipeline
enqueueOutreachJob(customerId, payload.funding_round)
res.status(200).send('ok')
})Python / FastAPI receiver
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = os.environ['FS_WEBHOOK_SECRET'] # fs_whsec_...
@app.post('/hooks/fundingscout')
async def fundingscout(request: Request):
raw_body = await request.body()
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
got = request.headers.get('X-FundingScout-Signature', '')
if not hmac.compare_digest(expected, got):
raise HTTPException(status_code=401, detail='invalid signature')
payload = await request.json()
customer_id = (payload['matched'].get('account_metadata') or {}).get('partner_customer_id')
# Route to the right end-customer's downstream pipeline
enqueue_outreach_job(customer_id, payload['funding_round'])
return {'ok': True}Respond with any 2xx status within 5 seconds. Slower = we mark the delivery failed (recoverable — see Step 5). Do heavy work async after returning 200.
Step 4 — Daily sync template
Run this once a day from your backend. Fresh snapshot of every end-customer's pipeline, batched into chunks of 1,000:
// Node.js cron at 2 AM daily — sync all customers' CRMs
async function dailyAccountSync() {
const allAccounts = []
for (const customer of await listOurCustomers()) {
const companies = await fetchCustomerCRM(customer.id)
for (const c of companies) {
allAccounts.push({
external_id: `${customer.id}-${c.id}`, // namespaced per-customer
name: c.name,
domain: c.domain,
metadata: { partner_customer_id: customer.id },
})
}
}
// Chunk into batches of 1,000 — the API's max per call
for (let i = 0; i < allAccounts.length; i += 1000) {
const batch = allAccounts.slice(i, i + 1000)
await fetch('https://fundingscout.io/api/v1/accounts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FS_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ accounts: batch }),
})
}
}5 customers × 1,000 companies each = 5 API calls per daily sync. Well under the 100/day default cap.
Step 5 — Recovery (optional)
If your webhook receiver was down for any reason, fetch missed matches the next day via the pull endpoint:
curl "https://fundingscout.io/api/v1/matches?status=failed&since=2026-05-15T00:00:00Z" \
-H "Authorization: Bearer $FS_KEY"
# Returns up to 200 matches per page. Use ?since=<next_cursor> to paginate.Multi-tenant routing pattern
The metadata tag round-trips through our system unchanged. Whatever you set on upload comes back on the webhook:
Upload (your → us):
POST /accounts
{ "metadata": { "partner_customer_id": "vc_42" } }
│
▼
[stored in our crm_accounts.metadata]
│
▼
Webhook (us → you):
POST your-url
{ "matched": { "account_metadata": { "partner_customer_id": "vc_42" } } }
│
▼
[your code routes by partner_customer_id]Put whatever you want in metadata — it's a JSONB pass-through. We don't look at the contents. Common additions: partner_customer_id, owner_email, tier, added_at.
Rate limits + scaling
| Endpoint | Default | Per call | Notes |
|---|---|---|---|
POST /accounts | 100/day | 1,000 records | 100k records/day available at default |
POST /contacts | 100/day | 1,000 records | Deferred for v1 — see below |
GET /matches | 100/day | 200 matches | Use for recovery, not primary delivery |
| Webhook delivery (us → you) | Unlimited | — | Realistic volume: 5–50 matches/day |
Need higher limits? Email api@fundingscout.io with your expected volume. We'll lift your per-key caps. No surprise bills — pricing changes are discussed before they hit.
What's not in v1
- Contacts sync is deferred. Most VCs track companies as accounts (with domains), so account-level matching catches the bulk of useful signals. We can add contacts later if you want to surface lower-pipeline relationships (people the VC met but hasn't formally added to pipeline).
- No CEO email guarantees. Our funding round payload includes
ceo_nameon most rounds andceo_emailon a subset (where enrichment has run). Don't architect around it being present 100% of the time. - No inbound CRM webhooks. We don't consume webhooks from HubSpot/Salesforce/etc. Daily push sync is the integration pattern. Sub-minute freshness isn't needed for our match cadence anyway — funding rounds happen on a slower-than-hourly clock.
- No OAuth into your customers' CRMs. You handle CRM connections on your side. We never touch your customers' Salesforce / HubSpot / DealCloud / Affinity directly — you stay in the middle.
Pricing
Pro tier ($89/mo) covers default usage. If your usage requires lifted rate limits, custom features, or dedicated infrastructure, we'll move you to a wholesale tier — discussed and agreed in writing before any pricing change. Email api@fundingscout.io with questions.
Reference
- POST /accounts — full field reference
- Webhooks — payload schema + signature verification details
- GET /matches — pull endpoint with cursor pagination
- How matches work — the three match paths + normalization rules
- Errors — HTTP status codes + error response shapes