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 want | Use |
|---|---|
| A table you built in Bellwork, as accounts with their people | This page: GET /api/v1/tables/{tableId}/records |
| Everyone your organization works with, across every table, kept current between runs | Syncing 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
- Create an API key under Settings → Integrations → API keys. It's shown once; keep it in your server's secrets. See Authentication.
- Find the table's id:
GET /api/v1/tableslists 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
domainfirst, then bynces_id.matchsays which one this district has. fieldsholds its values, keyed byfields[].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 inpeople. - 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. emailis 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.
Freshsales
Send Bellwork district tables to Freshsales as accounts, contacts or leads, and contact lists
Current rolling AI capacity GET
Returns a coarse state and a renewal date — deliberately never a balance, a count, or a percentage. Metered AI actions (chat, enrichment runs, and contact finds) draw against the trailing 168-hour allowance on the free tier. Each action returns exactly seven days after it was reserved; there is no calendar reset. Briefs are recorded but unmetered because follow slots already bound them. Enterprise and internal are unmetered and report `metered: false`. Clients should show nothing while `comfortable`, a quiet notice at `low`, and the upgrade path at `exhausted`. Requests refused for want of allowance return 402 from the acting endpoint, not from here.