Connect Data

Demo · Explore with sample dataCreate your own agent

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:

Customerin ChatGPT
Your agentHailera
Hailerabackend
Your endpointPOST /map
Your databasea view
  1. 1“Is my repair at 14 Grove St on?”

  2. 2look up: search jobs “14 Grove St”

  3. 3POST /map · Bearer secret · {op: search…}

  4. 4SELECT … FROM map_public_jobs

  5. 51 row

  6. 6200 {items: [job_1182]}

  7. 7declared fields only, as data

  8. 8“Yes — Thursday, 8:00 AM, Crew B.”

2Build it in 4 steps

  1. 1

    Add one POST route and check the secret

    Hailera sends the secret on every call. Anything without it gets a 401.

    server.js
    import 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. 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. 3

    Answer search — the few rows that match

    Search your view with the text and exact filters; return at most `limit` rows.

    server.js
    const 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. 4

    Answer get — one record or null

    The agent already has an id from a search. Return that row, or null.

    server.js
    if (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:

request body
{
  "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:

  1. Your replywhatever you return
  2. Cap25 rows at most
  3. Keep declared fieldsanything else is dropped
  4. Handed over as datanever as instructions
You returned
{ "id": "job_1182",
  "address": "14 Grove St",
  "status": "scheduled",
  "crew": "B",
  "customer_phone": "+1 201 555 0142",
  "internal_margin": 0.34 }
Your agent sees
{ "id": "job_1182",
  "address": "14 Grove St",
  "status": "scheduled",
  "crew": "B" }

// 2 fields not in your schema:
// dropped before the model sees them

5Test it

Try it from a terminal, then press Test under Connect data.

Terminal
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 ms

Secret accepted

Schema read — 3 tables

212 ms

Ready 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 body
{ "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.