Connect your data — developer guide
One POST endpoint on your server, three read-only operations, one secret. About a page of code; most people are done in an hour.
1How a look-up flows
Your endpoint is only ever called by Hailera's backend, during an answer. The model decides to look something up; Hailera makes the call. Follow the numbers:
2Build it in 4 steps
- 1
Add one POST route and check the secret
Hailera sends the secret on every call. Anything without it gets a 401.
server.jsimport express from "express"; const app = express(); app.use(express.json()); const SECRET = process.env.MAP_DATA_SECRET; // shown once by Hailera app.post("/map", (req, res) => { if (req.get("authorization") !== "Bearer " + SECRET) return res.status(401).json({ error: { code: "UNAUTHORIZED" } }); const { op } = req.body; // …answer schema, search or get (next steps) }); - 2
Answer schema — say what can be read
List each table your agent may read: a name, one sentence the model reads, and its columns. This is all your agent knows in advance.
server.js// What you're willing to expose. Written by hand, on purpose. const RESOURCES = { jobs: { description: "A customer's job by address or number.", fields: ["address", "status", "scheduled_for", "crew"], view: "map_public_jobs" }, // a view, not the table }; if (op === "schema") return res.json({ version: "1", service: "jobtrack", resources: Object.entries(RESOURCES).map(([name, r]) => ({ name, description: r.description, fields: r.fields })) }); - 3
Answer search — the few rows that match
Search your view with the text and exact filters; return at most `limit` rows.
server.jsconst spec = RESOURCES[req.body.resource]; // the lookup IS the allowlist if (!spec) return res.status(404).json({ error: { code: "RESOURCE_NOT_FOUND" } }); if (op === "search") { const { query = "", filters = {}, limit = 5 } = req.body; const rows = await db.searchView(spec.view, query, filters, Math.min(limit, 25)); return res.json({ items: rows }); } - 4
Answer get — one record or null
The agent already has an id from a search. Return that row, or null.
server.jsif (op === "get") { const row = await db.getFromView(spec.view, req.body.id); return res.json({ item: row ?? null }); } return res.status(400).json({ error: { code: "UNKNOWN_OP" } });
Expose a view, not the table
Make a read-only database role and a view with only the columns you listed. Even a bug in your handler can't leak more.
3The request, field by field
Every call is a JSON body. Here is a search, annotated:
{
"op": "search",
"version": "1",
"resource": "jobs",
"query": "14 Grove St",
"filters": {"status": "scheduled"},
"limit": 5
}- op
- schema · search · get — which question Hailera is asking.
- resource
- A name from your schema. Look it up in your allowlist; never use it as a SQL identifier.
- query
- Free text from the conversation. May be "".
- filters
- Exact-match pairs, up to 10. May be {}.
- limit
- How many rows. Default 5, never more than 25.
- opstringallalways sent
- version"1"allalways sent
- resourcestringsearch, getalways sent
- querystringsearch (may be "")always sent
- filtersobjectsearch (may be {})always sent
- limitnumbersearch (≤ 25)always sent
- idstringgetalways sent
4What Hailera does to your reply
Your reply never goes to the model as-is. Three things happen first:
- Your replywhatever you return
- Cap25 rows at most
- Keep declared fieldsanything else is dropped
- Handed over as datanever as instructions
{ "id": "job_1182",
"address": "14 Grove St",
"status": "scheduled",
"crew": "B",
"customer_phone": "+1 201 555 0142",
"internal_margin": 0.34 }{ "id": "job_1182",
"address": "14 Grove St",
"status": "scheduled",
"crew": "B" }
// 2 fields not in your schema:
// dropped before the model sees them5Test it
Try it from a terminal, then press Test under Connect data.
curl -X POST https://api.your-company.com/map \
-H "Authorization: Bearer $MAP_DATA_SECRET" \
-H "Content-Type: application/json" \
-d '{"op": "schema", "version": "1"}'Reached your address
86 msSecret accepted
Schema read — 3 tables
212 msReady for your agent
6Limits & errors
Hailera's limits win over anything in a request. When something fails, the agent answers without your data — and the owner sees why.
- timeout4 sPer call. Slower counts as unavailable.
- response size1 MBChecked while reading.
- calls per answer5Across all sources, for one customer question.
- rows per search25Default 5.
- filters10Exact-match pairs only.
- redirects0Point Hailera at the final address.
Secret refused
Put the current secret on your server, or make a new one.
401
Not found
The route isn't at this address, or the table name is unknown.
404
Your server failed
Reported; the agent says it couldn't check right now.
5xx / 4 s
{ "error": { "code": "RESOURCE_NOT_FOUND",
"message": "The requested resource is unavailable." } }7Security
- Read-only by design: there is no operation that changes anything.
- Hailera owns the address and the operation list — the model never picks a URL, a path or a method.
- Only the fields in your schema reach the model; the rest is dropped.
- The model never sees your address or secret — calls go server to server.
- Nothing is copied or stored. The log keeps table, operation, result count and time — never the customer's question.
Never pass `resource` to your database as a name
Look it up in your own allowlist (step 3). Using it as a table or column name is how a request becomes a query you didn't write.