Skip to content

Build a connector

A connector lets the assistant do things in another system: find an appointment, check room rates, place an order. It is a manifest plus, when needed, a small JavaScript file. This guide builds a connector for a shop’s order API.

{
"id": "com.example.shop",
"version": "1.0.0",
"name": "Example Shop",
"auth": { "type": "api_key", "header": "X-Api-Key" },
"config": {
"type": "object",
"required": ["base_url"],
"properties": { "base_url": { "type": "string", "title": "Shop address" } }
},
"permissions": { "hosts": ["{{config.base_url}}"], "scopes": ["records:write"] },
"actions": [
{
"name": "getPrice",
"description": "Look up the price and stock of a product by name or SKU.",
"risk": "read",
"confirm": "none",
"input": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" } } },
"output": { "type": "object", "properties": { "price": { "type": "number" }, "in_stock": { "type": "boolean" } } },
"http": {
"method": "GET",
"url": "{{config.base_url}}/products?q={{input.query}}",
"map": { "price": "$.items[0].price", "in_stock": "$.items[0].in_stock" }
}
},
{
"name": "placeOrder",
"description": "Place an order after the customer confirmed items, address and total.",
"risk": "money",
"confirm": "caller",
"input": {
"type": "object",
"required": ["items", "phone"],
"properties": {
"items": { "type": "array", "items": { "type": "object", "properties": { "sku": { "type": "string" }, "qty": { "type": "integer" } } } },
"phone": { "type": "string" },
"address": { "type": "string" }
}
},
"output": { "type": "object", "properties": { "order_id": { "type": "string" }, "total": { "type": "number" } } },
"js": "placeOrder"
}
]
}

getPrice is a declarative HTTP action. AirFone calls the URL, adds your API key header, and maps the reply with JSONPath. No code runs.

placeOrder needs a few calls in a row, so it is written in JavaScript.

index.js
export const actions = {
async placeOrder(input, ctx) {
const base = ctx.config.base_url;
const cart = await ctx.http.fetch(`${base}/carts`, { method: 'POST', json: { phone: input.phone } });
for (const item of input.items) {
await ctx.http.fetch(`${base}/carts/${cart.json.id}/items`, { method: 'POST', json: item });
}
const order = await ctx.http.fetch(`${base}/carts/${cart.json.id}/checkout`, {
method: 'POST',
json: { address: input.address },
});
ctx.log.info('order placed', { order_id: order.json.id });
await ctx.records.upsert({ phone: input.phone, type: 'order', id: order.json.id, data: order.json });
return { order_id: order.json.id, total: order.json.total };
},
};

ctx.http.fetch reaches only the hosts in permissions.hosts. The API key is added by AirFone and is never visible to your code.

Put the shop’s settings in .airfone/config.json and its API key in .airfone/secrets.json as { "api_key": "..." }. Keep both out of version control.

Terminal window
npx @airfone/connector dev getPrice --input '{"query":"rice"}' # one real call, in the same sandbox as AirFone
npx @airfone/connector test # review checks and every fixture, no network

dev uses the same QuickJS sandbox and limits as AirFone, and reaches only your declared hosts. Add --allow-local to call a mock server on your machine, and --watch to run again on every save. Fixtures go in an action’s fixtures in manifest.json, which AirFone’s review runs too, or in tests/*.json as { "action", "input", "expect" } for your machine only.

Terminal window
npx @airfone/connector keygen # once: creates your signing key and prints the public key
AIRFONE_TOKEN=... npx @airfone/connector publish # tests, packs, signs and uploads

Register the public key in the console under Connectors, Signing keys first. To upload in the console instead, run pack and sign and use Upload version. Automated review runs at once, and the results show next to the version.

See the connectors reference for every manifest field, the full ctx API and limits.