Bellwork

Your own CRM

Pull Bellwork district tables into a CRM you built, as accounts and people

If your CRM is one you built, it pulls a table from Bellwork's API instead of Bellwork writing to it. What it gets is what Bellwork sends to Salesforce and Freshsales: each district as an account with the table's answers as fields, and its people. It's built by the same code, so a table reads the same in every CRM.

Bellwork never writes to your CRM. Your job reads, and you decide what to keep.

There are two ways to pull, and most CRMs start with the first:

You wantUse
A table you built in Bellwork, as accounts with their peopleThis page: GET /api/v1/tables/{tableId}/records
Everyone your organization works with, across every table, kept current between runsSyncing contacts to a CRM: GET /api/v1/contacts?scope=org

Both identify a person by the same Bellwork contact id (contact_id in a table's people, id on a contact), so a CRM can use either or both without making duplicates.

Before you start

  1. Create an API key under Settings → Integrations → API keys. It's shown once; keep it in your server's secrets. See Authentication.
  2. Find the table's id: GET /api/v1/tables lists your tables with their projects.

Only district tables come out as records, as only district tables sync to a CRM.

Pull a table

curl -H "Authorization: Bearer $BELLWORK_API_KEY" \
  'https://www.bellwork.ai/api/v1/tables/{tableId}/records?limit=25&offset=0'

Rows come in the table's own order, up to 100 a page. Ask again with a larger offset until pagination.has_more is false.

{
  "table": { "id": "…", "name": "TX counseling" },
  "fields": [
    { "key": "bellwork_enrollment", "label": "Enrollment", "kind": "number", "source_id": "fact:enrollment" },
    { "key": "bellwork_superintendent_person", "label": "Superintendent", "kind": "person", "source_id": "fact:superintendent" },
    { "key": "bellwork_c_ratio", "label": "Counselor ratio", "kind": "text", "source_id": "column:ratio" }
  ],
  "data": [
    {
      "id": "c5f1…",
      "account": {
        "name": "Katy ISD",
        "nces_id": "4825170",
        "domain": "katyisd.org",
        "match": { "by": "domain", "domain": "katyisd.org" },
        "fields": {
          "bellwork_enrollment": 94000,
          "bellwork_superintendent_person": ["8a1e…"],
          "bellwork_c_ratio": "1 counselor per 412 students"
        }
      },
      "people": [
        {
          "contact_id": "8a1e…",
          "name": "Ken Gregorski",
          "email": "[email protected]",
          "title": "Superintendent",
          "phone": null,
          "linkedin_url": null,
          "fields": ["bellwork_superintendent_person"]
        }
      ],
      "warnings": [],
      "error": null,
      "hash": "9b2c…"
    }
  ],
  "pagination": { "total": 212, "limit": 25, "offset": 0, "has_more": true }
}

What each part is

fields lists every field a record can carry: the district's facts (enrollment, schools, address, superintendent, Bellwork link) and every column of the table. key is stable for a table; use it as your field's name.

account is the district.

  • Match it to your accounts the way Bellwork's CRM sync does: by website domain first, then by nces_id. match says which one this district has.
  • fields holds its values, keyed by fields[].key:
    • A number, text or yes/no is the value.
    • The mailing address is an object.
    • A list answer is a list of its items.
    • A person field is the list of contact_ids it names, each of whom is in people.
    • An empty answer is left out, as Bellwork never clears a field in a CRM.

people are everyone the table's person columns name, plus the superintendent: each once, with the fields that name them.

  • Key them on contact_id, which never changes, and link each to the account.
  • email is lowercased and never one that bounced.
  • Someone without an email is still here, recognizable by contact_id.

error says why a row can't be an account (no website or NCES ID to match on); such a row has no people. warnings list what was left out and why.

Keep it current

Run your job on a schedule and store each record's hash with your account. When a record's hash matches the one you stored, nothing in it changed: skip it. This is how Bellwork's own CRM sync skips unchanged rows.

A table re-reads its rows on every pull, so run the job as often as you want the CRM to catch up: daily is plenty for most teams. To catch a person's new email or title between table runs, also run Syncing contacts to a CRM with scope=org; it returns only the people who changed.

Limits

Each page reads its districts' people and answers, so it takes a few seconds. Keep limit at 25 to 50 for a steady job. Your plan allows 5,000 requests a minute; see Rate limits.