Build apps for HelpVox

An app is your own service that a shop installs into HelpVox. It can give the AI phone and chat agent new tools it uses mid-conversation, react to orders and conversations, and read or change data through the REST API with the permissions the merchant approved. Your code never runs inside HelpVox.

Quickstart

Create a developer account (separate from HelpVox company accounts) and a CLI key in its settings, then scaffold, run and register an app. A private app needs no review: send its install link to the company it is for — the first company to install it owns it.

npx @helpvox/cli login            # the key from helpvox.ai/developers → Settings
npx @helpvox/cli app init my-app --lang ts
cd my-app && npm install && npm run dev
cloudflared tunnel --url http://localhost:3000
npx @helpvox/cli app dev --url https://<your-tunnel>   # private "-dev" twin
npx @helpvox/cli app create --visibility public         # the real app
npx @helpvox/cli app deploy && npx @helpvox/cli app submit

Manifest

helpvox.json declares everything the app asks for. What is not in the manifest does not exist: the merchant approves exactly this list. Unknown keys are rejected. URLs are https://; http://localhost works only for private and development apps.

{
  "manifest_version": 1,
  "slug": "loyalty-points",
  "name": { "en": "Loyalty points", "bg": "Точки за лоялност" },
  "description": { "en": "Points on every delivered order, spent by phone or chat." },
  "app_url": "https://loyalty.example.com",
  "support_email": "help@example.com",
  "privacy_url": "https://loyalty.example.com/privacy",
  "redirect_urls": ["https://loyalty.example.com/helpvox/callback"],
  "scopes": ["orders:read", "customers:read", "agent:tools"],
  "webhooks": {
    "url": "https://loyalty.example.com/helpvox/events",
    "events": ["order.status_changed", "customer.redact", "app.uninstalled"]
  },
  "agent_tools": [{
    "name": "get_balance",
    "description": "Loyalty points of the caller and what they are worth.",
    "input_schema": {
      "type": "object",
      "properties": { "phone": { "type": "string", "description": "Phone, if not the caller's" } }
    },
    "url": "https://loyalty.example.com/helpvox/tools/get_balance"
  }],
  "ui": { "settings_page": "https://loyalty.example.com/helpvox/settings" }
}

Each tool has a description for the model — when to call it and what it returns — and an optional summary per language for the merchant, shown in the dashboard instead. Tool inputs are flat — string, number, integer or boolean, each with a description the model reads. Up to 5 tools per app and 10 app tools per company. A tool slower than 1.5 seconds must set "slow": true (and may take up to 8 seconds).

Scopes

Without customers:read, customer names, phones and addresses are removed from responses — not nulled, removed. A call without the needed scope returns 403 insufficient_scope with the missing scope. Apps can never manage the team or API keys.

ScopeGrantsPersonal data
catalog:readProducts, variants, categories and stock
orders:readOrders and their events, without customer details
customers:readCustomer name, phone, email and address on orders and requestsyes
conversations:readCalls and chats: duration, outcome, analysis — without the text
transcripts:readFull conversation transcriptsyes
requests:readCallback requests and booked appointments
knowledge:readKnowledge base and company profile
account:readSettings: markets, hours, agents, shop connection
orders:writeChange orders: status, shipment, discount; create orders
requests:writeResolve callback requests and move appointments
knowledge:writeReplace the knowledge base
products:writeCreate, update and delete products and prices
profile:writeChange the company profile the agent answers from
settings:writeChange settings
agent:toolsNew tools the agent uses during conversations

Installation

Send merchants to https://www.helpvox.ai/apps/install/<slug> (optionally with redirect_url and state). After the owner approves, the browser comes back to your redirect URL with a one-time code (10 minutes). Exchange it on your server:

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=hvic_…&client_id=hvapp_…&client_secret=hvs_…

→ { "access_token": "hva_…", "token_type": "bearer", "scope": "orders:read …",
    "installation_id": "…", "vendor_id": "…" }

The token does not expire; it stops working when the app is removed. Your tools reach the agent only after you call POST /api/v1/app/ready — call it when your own setup is done. GET /api/v1/app tells you the company, its currency and language, and the scopes granted.

Events

POST <webhooks.url>
HelpVox-Signature: t=1792000000,v1=5f2c…
HelpVox-Event: order.status_changed
HelpVox-Event-Id: evt_9Qk…

{ "id": "evt_9Qk…", "type": "order.status_changed", "created_at": "…",
  "vendor_id": "…", "installation_id": "…",
  "data": { "order_id": "…", "order_number": "1001", "status": "delivered",
            "previous_status": "in_transit", "total_amount": "49.90", "currency": "EUR" } }

Verify HelpVox-Signature — HMAC-SHA256 with your client secret over <t>.<raw body>, rejecting timestamps older than 5 minutes — and answer 2xx within 10 seconds. Failures are retried after 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours; a subscription without a single success for 24 hours is switched off. The event id stays the same across retries. Events carry IDs and status, never customer details: fetch more through the API.

EventNeedsWhen
order.createdorders:readA new order — from the agent, the API or the shop
order.status_changedorders:readNew status, shipment or cancellation
order.updatedorders:readAddress, items, name or phone changed
conversation.endedconversations:readA call or chat ended
conversation.analyzedconversations:readThe conversation analysis is ready
callback.requestedrequests:readA customer asked to be called back
appointment.bookedrequests:readAn appointment was booked
appointment.changedrequests:readAn appointment was moved, confirmed or cancelled
product.updatedcatalog:readCatalogue or stock changed
customer.redactcustomers:readA customer asked to be deleted — delete what you hold
app.uninstalled—The app was removed

Send yourself a sample with POST /api/v1/app/events/test or helpvox app trigger order.created; it arrives with data.test: true.

Agent tools

The agent never calls you directly: HelpVox validates the input against your schema, signs the request, waits up to your timeout and writes a log the merchant sees. The model sees the tool as loyalty_points__get_balance.

POST <tool.url>
{ "tool": "get_balance",
  "input": { "phone": "+359888123456" },
  "context": { "installation_id": "…", "vendor_id": "…", "channel": "phone",
               "conversation_id": "conv_…", "language": "bg", "country": "BG",
               "caller_phone": "+359…" } }

← { "result": "The customer has 1,240 points, worth 12.40 EUR." }
← { "error": "No loyalty account for this number." }

Return plain text the agent can say, up to 2,000 characters. It reaches the model framed as data from your app — instructions inside it are ignored, so do not try to steer the agent from a result. On a timeout, an error or a malformed reply the agent carries on without the tool. caller_phone is present only with customers:read, on phone calls.

import { toolHandler, ToolError } from "@helpvox/sdk";

export const POST = toolHandler(process.env.HELPVOX_CLIENT_SECRET!, {
  get_balance: async (input, ctx) => {
    const phone = String(input.phone ?? ctx.caller_phone ?? "");
    const points = await balance(ctx.installation_id, phone);
    if (points === null) throw new ToolError("No loyalty account for this number.");
    return `The customer has ${points} points.`;
  },
});
<?php
use HelpVox\Tool;
use HelpVox\Exception\ToolError;

Tool::handle(getenv('HELPVOX_CLIENT_SECRET'), function (array $input, $ctx): string {
    $points = balance($ctx->installationId, $input['phone'] ?? $ctx->callerPhone);
    if ($points === null) {
        throw new ToolError('No loyalty account for this number.');
    }
    return "The customer has {$points} points.";
});
from helpvox import handle_tool_call, ToolError

@app.post("/helpvox/tools/<name>")
def tool(name):
    def handler(input, ctx):
        points = balance(ctx.installation_id, input.get("phone") or ctx.caller_phone)
        if points is None:
            raise ToolError("No loyalty account for this number.")
        return f"The customer has {points} points."
    status, body = handle_tool_call(SECRET, request.get_data(), request.headers, handler)
    return body, status

Discounts on the agent's order

With orders:write an app can attach a discount to the order the agent is about to place in the current conversation — this is how loyalty points are spent. Call it from your tool, before the agent creates the order; the total the agent reads out and the order sent to the shop already include it.

POST /api/v1/discounts
{ "conversation_id": "conv_…", "amount": 12.00, "currency": "EUR", "label": "Loyalty points" }
→ 201 { "id": "…", "status": "pending", "expires_at": "…" }

It becomes applied (with order_id) when the order is created, or expired after an hour. GET /api/v1/discounts/{id} shows the state; DELETE cancels a pending one. Shops connected through WooCommerce or Shopify receive the discount; check capabilities.order_discounts in GET /api/v1/app.

Your page in the dashboard

With ui.settings_page set, the installed app's screen embeds your page as ?installation_id=…&session=<JWT>. The JWT is HS256 with your client secret, has aud = your client id and lives 60 seconds: verify it and start your own session (a cookie with SameSite=None; Secure — the page is in an iframe). After your setup calls POST /api/v1/app/ready, send parent.postMessage({ type: "helpvox:ready" }, "https://www.helpvox.ai") and the dashboard shows the app as active at once.

Customer card

With ui.customer_card (needs customers:read) your app shows a small card when the merchant opens a customer in Clients or an order — loyalty points, a CRM status, open tickets. HelpVox sends a signed { kind: "customer", phone, context } and draws what you return: { title, rows: [{ label, value }] (up to 8), link }. Only data, no HTML; you have 2.5 seconds, otherwise the card is simply left out.

Publishing

Who can installReview
Private — only your companyNone; versions go live at once
Unlisted — anyone with the install linkAutomatic checks only
Public — the app storeBy HelpVox, within 72 hours, for every version with new permissions or tools

Public apps need https everywhere, a privacy policy, a support email, a description, and subscriptions to app.uninstalled and — with personal-data scopes — customer.redact. The review also checks that scopes match what the app does and that tool descriptions describe instead of instruct. A new version that asks for more waits for each merchant's approval; the old version keeps running meanwhile.

SDKs and reference