Data Connectors
Connect your API endpoints to Luca so it can look up orders, check inventory, verify accounts, and more during live conversations.
Overview
Data Connectors let you connect your own API endpoints to Luca, the Helpr AI agent. When a visitor asks a question that requires live data — order status, account balance, stock availability — Luca calls your endpoint, reads the response, and answers the visitor in natural language.
Under the hood, Data Connectors use AI-powered function calling. Each connector you create becomes a tool that Luca can invoke during a conversation. You define the tool name, description, parameters, and the URL to call. Luca decides when to use it based on the conversation context.
Quick Start
- Go to Settings → AI Agent → Data Connectors in the Helpr dashboard
- Click Add connector
- Fill in the tool definition:
| Field | Example | Description |
|---|---|---|
Name | order_lookup | Unique identifier (snake_case, max 64 chars) |
Description | Look up an order by order number and return its status, tracking info, and delivery estimate. | Tells Luca when and why to use this tool |
Endpoint URL | https://api.yourstore.com/helpr/order-lookup | The URL Helpr will call |
Method | POST | GET or POST |
Requires Identity | Off | When enabled, Helpr includes verified visitor identity |
Define parameters
Add parameters using the visual builder in the modal. Each parameter maps to a JSON Schema property that Luca fills in from the conversation.
{
"type": "object",
"properties": {
"order_number": {
"type": "string",
"description": "The order number, e.g. ORD-12345"
}
},
"required": ["order_number"]
}
- Copy your Signing Secret from the Data Connectors tab (format:
hlpr_wh_+ 48 hex chars) - Deploy your endpoint and send a test request from the dashboard
Request Signing
Helpr signs every outbound request with HMAC-SHA256 so you can verify it originated from Helpr and has not been tampered with.
Headers
| Header | Description | Example |
|---|---|---|
X-Helpr-Timestamp | Unix epoch seconds when the request was signed | 1714003200 |
X-Helpr-Signature | HMAC-SHA256 signature of the canonical string | sha256=a1b2c3... |
Content-Type | Always application/json for POST requests | application/json |
Canonical string
The signature is computed over a canonical string built from four components joined by newlines:
{METHOD}\n{PATH}\n{TIMESTAMP}\n{SHA256_OF_BODY}
| Component | Description |
|---|---|
METHOD | HTTP method, uppercase (POST or GET) |
PATH | Request path including query string (e.g. /helpr/order-lookup) |
TIMESTAMP | Same value as the X-Helpr-Timestamp header |
SHA256_OF_BODY | Hex-encoded SHA-256 hash of the raw request body |
Signature format
sha256= + HMAC-SHA256(canonical_string, signing_secret)
The HMAC is hex-encoded.
Verification steps
- Check the timestamp — reject requests where
X-Helpr-Timestampis more than 5 minutes from your server's current time - Rebuild the canonical string from the incoming request
- Compute the expected HMAC using your signing secret
- Compare signatures using a constant-time comparison function to prevent timing attacks
Request Format
POST requests
The request body is JSON. Tool parameters are top-level keys:
{
"order_number": "ORD-12345"
}
GET requests
Parameters are sent as query string parameters:
GET /helpr/check-stock?sku=WIDGET-42&warehouse=us-east
Identity Object
When a connector has Requires Identity enabled and the visitor is verified (via helpr.identify() with a resolved contact), Helpr includes a helpr_identity object:
{
"order_number": "ORD-12345",
"helpr_identity": {
"email": "[email protected]",
"name": "Jane Smith",
"verified": true,
"custom": { "plan": "enterprise", "account_id": "acc_123" }
}
}
| Field | Type | Description |
|---|---|---|
email | string | Visitor's verified email address |
name | string | Visitor's display name |
verified | boolean | Always true when present |
custom | object | Custom data set via helpr.setCustom() or the API |
Visitor Object
Helpr automatically includes a helpr_visitor object with session context on every connector request, regardless of identity settings:
{
"order_number": "ORD-12345",
"helpr_visitor": {
"current_url": "https://yoursite.com/products/widget",
"country": "CA",
"city": "Toronto",
"timezone": "America/Toronto",
"language": "en",
"device": "desktop"
}
}
| Field | Type | Description |
|---|---|---|
current_url | string | The page the visitor is currently on |
country | string | Two-letter country code (ISO 3166-1) |
city | string | Visitor's city (from IP geolocation) |
timezone | string | IANA timezone identifier |
language | string | Browser language code |
device | string | Device type: desktop, mobile, or tablet |
helpr_visitor object is always present (even without identity verification), so your endpoint can use it for location-aware responses like showing nearby store stock.
Response Format
Return JSON from your endpoint. Maximum response size is 4 KB — anything beyond that is truncated.
Standard response
Return any JSON object. Luca reads the data and formulates a natural-language answer for the visitor.
{
"order_id": "ORD-12345",
"status": "shipped",
"tracking_number": "1Z999AA10123456784",
"estimated_delivery": "2026-04-28"
}
Luca will say something like: “Your order ORD-12345 has shipped! The tracking number is 1Z999AA10123456784 and it's estimated to arrive on April 28th.”
Challenge-Response Protocol
If your endpoint needs to verify the visitor's identity before releasing data, return a helpr_challenge array instead of the data. Each item defines a question for Luca to ask:
{
"helpr_challenge": [
{
"field": "shipping_zip",
"prompt": "What is the shipping postal/zip code for this order?"
},
{
"field": "last_four",
"prompt": "What are the last 4 digits of the card used?",
"type": "number"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
field | string | Yes | Key name for the answer (returned in helpr_challenge_response) |
prompt | string | Yes | The question Luca asks the visitor |
type | string | No | Expected answer type: string (default), number, email |
Luca asks the visitor naturally, collects answers, and retries with helpr_challenge_response:
{
"order_number": "ORD-12345",
"helpr_identity": { ... },
"helpr_challenge_response": {
"shipping_zip": "90210"
}
}
Full flow
2. Luca calls your endpoint with
{ "order_number": "ORD-12345" }3. Your endpoint returns
helpr_challenge asking for shipping zip4. Luca asks the visitor: "Could you share the zip code for the shipping address?"
5. Visitor: "90210"
6. Luca retries with
helpr_challenge_response: { "shipping_zip": "90210" }7. Your endpoint validates → returns order data (or 403 if wrong)
Your endpoint is stateless — it receives the full context (parameters, identity, and challenge answers) on every call.
Error Handling
Return an HTTP 4xx status code with an error or message field:
{ "error": "Order not found" }
Luca relays the error naturally: “I wasn't able to find that order number. Could you double-check it?”
| Status Code | Usage |
|---|---|
400 | Bad request / invalid parameters |
401 | Identity required but not provided |
403 | Challenge verification failed |
404 | Resource not found |
429 | Rate limited — Luca tells the visitor to try again later |
Signature Verification
const crypto = require('crypto');
function verifyHelprSignature(req, secret) {
const timestamp = req.headers['x-helpr-timestamp'];
const signature = req.headers['x-helpr-signature'];
// Reject if older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const body = JSON.stringify(req.body);
const path = req.originalUrl || req.url;
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const canonical = `POST\n${path}\n${timestamp}\n${bodyHash}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret).update(canonical).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(expected)
);
}
import hashlib, hmac, time
def verify_helpr_signature(request, secret: str) -> bool:
timestamp = request.headers.get('X-Helpr-Timestamp', '')
signature = request.headers.get('X-Helpr-Signature', '')
if abs(time.time() - int(timestamp)) > 300:
return False
body = request.get_data(as_text=True)
path = request.path
body_hash = hashlib.sha256(body.encode()).hexdigest()
canonical = f"POST\n{path}\n{timestamp}\n{body_hash}"
expected = 'sha256=' + hmac.new(
secret.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
function verifyHelprSignature(string $secret): bool {
$timestamp = $_SERVER['HTTP_X_HELPR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HELPR_SIGNATURE'] ?? '';
if (abs(time() - (int) $timestamp) > 300) return false;
$body = file_get_contents('php://input');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$canonical = "POST\n{$path}\n{$timestamp}\n" . hash('sha256', $body);
$expected = 'sha256=' . hash_hmac('sha256', $canonical, $secret);
return hash_equals($expected, $signature);
}
require 'openssl'
def verify_helpr_signature(request, secret)
timestamp = request.headers['X-Helpr-Timestamp']
signature = request.headers['X-Helpr-Signature']
return false if (Time.now.to_i - timestamp.to_i).abs > 300
body = request.body.read
path = request.path
body_hash = OpenSSL::Digest::SHA256.hexdigest(body)
canonical = "POST\n#{path}\n#{timestamp}\n#{body_hash}"
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, canonical)
Rack::Utils.secure_compare(expected, signature)
end
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"net/http"
"strconv"
"time"
)
func verifyHelprSignature(r *http.Request, secret string) bool {
timestamp := r.Header.Get("X-Helpr-Timestamp")
signature := r.Header.Get("X-Helpr-Signature")
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return false
}
bodyHash := sha256.Sum256(body)
canonical := fmt.Sprintf("POST\n%s\n%s\n%x", r.URL.Path, timestamp, bodyHash)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
Full Example: Order Lookup
A complete endpoint with HMAC verification, identity checks, challenge-response, and data retrieval.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const HELPR_SECRET = process.env.HELPR_WEBHOOK_SECRET;
function verifyHelpr(req, res, next) {
const timestamp = req.headers['x-helpr-timestamp'];
const signature = req.headers['x-helpr-signature'];
if (!timestamp || !signature) {
return res.status(401).json({ error: 'Missing signature' });
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).json({ error: 'Request too old' });
}
const body = JSON.stringify(req.body);
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const canonical = `POST\n${req.originalUrl}\n${timestamp}\n${bodyHash}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', HELPR_SECRET).update(canonical).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
app.post('/helpr/order-lookup', verifyHelpr, async (req, res) => {
const { order_number, helpr_identity, helpr_challenge_response } = req.body;
const order = await db.orders.findOne({ orderNumber: order_number });
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
if (!helpr_identity) {
return res.status(401).json({ error: 'Identity required' });
}
// Challenge if no verification yet
if (!helpr_challenge_response) {
return res.json({
helpr_challenge: [{
field: 'shipping_zip',
prompt: 'What is the shipping zip code for this order?'
}]
});
}
// Validate the answer
if (helpr_challenge_response.shipping_zip !== order.shippingAddress.zip) {
return res.status(403).json({ error: 'Zip code does not match' });
}
res.json({
order_id: order.orderNumber,
status: order.status,
tracking_number: order.trackingNumber || null,
estimated_delivery: order.estimatedDelivery || null,
items: order.items.map(i => ({ name: i.name, qty: i.quantity })),
total: '$' + order.total.toFixed(2)
});
});
app.listen(3000);
import hashlib, hmac, json, time
from flask import Flask, request, jsonify
app = Flask(__name__)
HELPR_SECRET = os.environ['HELPR_WEBHOOK_SECRET']
def verify_helpr(f):
def wrapper(*args, **kwargs):
timestamp = request.headers.get('X-Helpr-Timestamp', '')
signature = request.headers.get('X-Helpr-Signature', '')
if abs(time.time() - int(timestamp)) > 300:
return jsonify(error='Request too old'), 401
body = request.get_data(as_text=True)
body_hash = hashlib.sha256(body.encode()).hexdigest()
canonical = f"POST\n{request.path}\n{timestamp}\n{body_hash}"
expected = 'sha256=' + hmac.new(
HELPR_SECRET.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify(error='Invalid signature'), 401
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
@app.route('/helpr/order-lookup', methods=['POST'])
@verify_helpr
def order_lookup():
data = request.json
order_number = data.get('order_number')
identity = data.get('helpr_identity')
challenge_resp = data.get('helpr_challenge_response')
order = db.orders.find_one({'order_number': order_number})
if not order:
return jsonify(error='Order not found'), 404
if not identity:
return jsonify(error='Identity required'), 401
# Challenge if no verification yet
if not challenge_resp:
return jsonify(helpr_challenge=[{
'field': 'shipping_zip',
'prompt': 'What is the shipping zip code for this order?'
}])
# Validate the answer
if challenge_resp.get('shipping_zip') != order['shipping_zip']:
return jsonify(error='Zip code does not match'), 403
return jsonify(
order_id=order['order_number'],
status=order['status'],
tracking_number=order.get('tracking_number'),
estimated_delivery=order.get('estimated_delivery'),
total=f"${order['total']:.2f}"
)
<?php
$secret = $_ENV['HELPR_WEBHOOK_SECRET'];
// Verify signature
$timestamp = $_SERVER['HTTP_X_HELPR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HELPR_SIGNATURE'] ?? '';
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
echo json_encode(['error' => 'Request too old']);
exit;
}
$rawBody = file_get_contents('php://input');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$canonical = "POST\n{$path}\n{$timestamp}\n" . hash('sha256', $rawBody);
$expected = 'sha256=' . hash_hmac('sha256', $canonical, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($rawBody, true);
$orderNumber = $data['order_number'] ?? '';
$identity = $data['helpr_identity'] ?? null;
$challengeResp = $data['helpr_challenge_response'] ?? null;
$order = $db->orders->findOne(['order_number' => $orderNumber]);
if (!$order) {
http_response_code(404);
echo json_encode(['error' => 'Order not found']);
exit;
}
if (!$identity) {
http_response_code(401);
echo json_encode(['error' => 'Identity required']);
exit;
}
// Challenge if no verification yet
if (!$challengeResp) {
echo json_encode(['helpr_challenge' => [[
'field' => 'shipping_zip',
'prompt' => 'What is the shipping zip code for this order?',
]]]);
exit;
}
// Validate the answer
if (($challengeResp['shipping_zip'] ?? '') !== $order['shipping_zip']) {
http_response_code(403);
echo json_encode(['error' => 'Zip code does not match']);
exit;
}
echo json_encode([
'order_id' => $order['order_number'],
'status' => $order['status'],
'tracking_number' => $order['tracking_number'] ?? null,
'estimated_delivery' => $order['estimated_delivery'] ?? null,
'total' => '$' . number_format($order['total'], 2),
]);
require 'sinatra'
require 'json'
require 'openssl'
HELPR_SECRET = ENV['HELPR_WEBHOOK_SECRET']
before '/helpr/*' do
request.body.rewind
@raw_body = request.body.read
timestamp = request.env['HTTP_X_HELPR_TIMESTAMP']
signature = request.env['HTTP_X_HELPR_SIGNATURE']
if (Time.now.to_i - timestamp.to_i).abs > 300
halt 401, { error: 'Request too old' }.to_json
end
body_hash = OpenSSL::Digest::SHA256.hexdigest(@raw_body)
canonical = "POST\n#{request.path}\n#{timestamp}\n#{body_hash}"
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', HELPR_SECRET, canonical)
unless Rack::Utils.secure_compare(expected, signature)
halt 401, { error: 'Invalid signature' }.to_json
end
@data = JSON.parse(@raw_body)
end
post '/helpr/order-lookup' do
content_type :json
order = DB[:orders].where(order_number: @data['order_number']).first
halt 404, { error: 'Order not found' }.to_json unless order
identity = @data['helpr_identity']
halt 401, { error: 'Identity required' }.to_json unless identity
challenge_resp = @data['helpr_challenge_response']
# Challenge if no verification yet
unless challenge_resp
return { helpr_challenge: [{
field: 'shipping_zip',
prompt: 'What is the shipping zip code for this order?'
}] }.to_json
end
# Validate the answer
if challenge_resp['shipping_zip'] != order[:shipping_zip]
halt 403, { error: 'Zip code does not match' }.to_json
end
{
order_id: order[:order_number],
status: order[:status],
tracking_number: order[:tracking_number],
estimated_delivery: order[:estimated_delivery],
total: "$#{'%.2f' % order[:total]}"
}.to_json
end
Best Practices
Tool naming and descriptions
Luca uses the tool name and description to decide when to call your endpoint. Write descriptions as if explaining to a support agent what this tool does and when to use it. Be specific:
Bad: “Get order info.”
Keep responses small
The 4 KB limit exists because the response is injected into Luca's context window. Return only the data Luca needs to answer the visitor. Omit internal IDs, audit logs, and raw database fields.
Use challenge-response for PII
Never return sensitive data (addresses, payment details, order history) without verifying the visitor. Use helpr_challenge to ask a verification question even if helpr_identity is present — the identity confirms who they claim to be, the challenge confirms they own the resource.
Set appropriate timeouts
Your endpoint must respond within 10 seconds. If your data source is slow, consider caching or returning a simplified response. Luca shows a typing indicator while waiting.
Return descriptive errors
Error messages in the error field are shown to the visitor (paraphrased by Luca). “No order found with that number” is better than “404” or “null reference exception.”
Idempotent endpoints
Helpr may retry a request if the first attempt times out. Your endpoint should be safe to call multiple times with the same parameters.
HTTPS only
Helpr only calls endpoints over HTTPS. Plain HTTP URLs are rejected at configuration time.