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:

  1. 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.
  2. We continuously monitor funding rounds (press releases, SEC filings, RSS feeds).
  3. 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 an fs_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

EndpointDefaultPer callNotes
POST /accounts100/day1,000 records100k records/day available at default
POST /contacts100/day1,000 recordsDeferred for v1 — see below
GET /matches100/day200 matchesUse for recovery, not primary delivery
Webhook delivery (us → you)UnlimitedRealistic 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_name on most rounds and ceo_email on 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