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 | <unix_epoch_seconds> |
X-Helpr-Signature | HMAC-SHA256 signature of the timestamp and exact raw request body bytes | sha256=a1b2c3... |
Content-Type | Always application/json for POST requests | application/json |
Signed bytes and signature format
This Data Connector callback contract signs the timestamp, a literal ASCII period, and the exact request body bytes:
signed_payload = ASCII(X-Helpr-Timestamp) + "." + raw_request_body_bytes
X-Helpr-Signature = "sha256=" + lowercase_hex(HMAC-SHA256(signing_secret, signed_payload))
Do not parse, reformat, or re-serialize the JSON before verification: whitespace, escaping, and key order are part of the signed body. Capture the raw bytes before a framework body parser consumes them. The HTTP method and URL path are not part of this Data Connector signature. Platform API authentication and event webhooks use separate signing contracts documented in their own references.
Verification steps
- Validate the headers — require a decimal Unix timestamp and a
sha256=value containing 64 lowercase hexadecimal characters - Check the timestamp — reject requests more than 5 minutes in the past or future
- Build the signed payload from the timestamp header, a period, and the unmodified raw body bytes
- Compute the expected HMAC using your Data Connector 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'];
const rawBody = req.rawBody; // Capture as a Buffer in the JSON parser's verify hook.
if (!/^[1-9][0-9]*$/.test(timestamp || '') ||
!/^sha256=[a-f0-9]{64}$/.test(signature || '') ||
!Buffer.isBuffer(rawBody)) {
return false;
}
const signedAt = Number(timestamp);
if (!Number.isSafeInteger(signedAt) ||
Math.abs(Math.floor(Date.now() / 1000) - signedAt) > 300) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.', 'ascii')
.update(rawBody)
.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 (not timestamp or len(timestamp) > 12
or not timestamp.isascii() or not timestamp.isdecimal()
or timestamp[0] == '0' or len(signature) != 71
or not signature.startswith('sha256=')
or any(c not in '0123456789abcdef' for c in signature[7:])):
return False
if abs(int(time.time()) - int(timestamp)) > 300:
return False
body = request.get_data(cache=True) # bytes; do not decode or re-serialize
signed_payload = timestamp.encode('ascii') + b'.' + body
expected = 'sha256=' + hmac.new(
secret.encode(), signed_payload, 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 (!preg_match('/^[1-9][0-9]*$/D', $timestamp)
|| !preg_match('/^sha256=[a-f0-9]{64}$/D', $signature)
|| abs(time() - (int) $timestamp) > 300) return false;
$body = file_get_contents('php://input');
if ($body === false) return false;
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $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 unless timestamp&.match?(/\A[1-9][0-9]*\z/)
return false unless signature&.match?(/\Asha256=[a-f0-9]{64}\z/)
return false if (Time.now.to_i - timestamp.to_i).abs > 300
request.body.rewind
body = request.body.read
request.body.rewind
signed_payload = timestamp.b + '.'.b + body.b
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
Rack::Utils.secure_compare(expected, signature)
end
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
)
func verifyHelprSignature(r *http.Request, secret string) bool {
timestamp := r.Header.Get("X-Helpr-Timestamp")
signature := r.Header.Get("X-Helpr-Signature")
if timestamp == "" || timestamp[0] == '0' ||
strings.Trim(timestamp, "0123456789") != "" ||
len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") ||
strings.Trim(signature[7:], "0123456789abcdef") != "" {
return false
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
now := time.Now().Unix()
if err != nil || ts <= 0 || ts < now-300 || ts > now+300 {
return false
}
body, err := io.ReadAll(r.Body)
if err != nil {
return false
}
r.Body = io.NopCloser(bytes.NewReader(body)) // Preserve bytes for JSON decoding.
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(body)
provided, err := hex.DecodeString(strings.TrimPrefix(signature, "sha256="))
return err == nil && len(provided) == sha256.Size && hmac.Equal(provided, mac.Sum(nil))
}
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({
verify: (req, res, buf) => { req.rawBody = Buffer.from(buf); }
}));
const HELPR_SECRET = process.env.HELPR_DATA_CONNECTOR_SECRET;
function verifyHelpr(req, res, next) {
const timestamp = req.headers['x-helpr-timestamp'];
const signature = req.headers['x-helpr-signature'];
if (!/^[1-9][0-9]*$/.test(timestamp || '') ||
!/^sha256=[a-f0-9]{64}$/.test(signature || '') ||
!Buffer.isBuffer(req.rawBody)) {
return res.status(401).json({ error: 'Missing or malformed signature' });
}
const signedAt = Number(timestamp);
if (!Number.isSafeInteger(signedAt) ||
Math.abs(Math.floor(Date.now() / 1000) - signedAt) > 300) {
return res.status(401).json({ error: 'Request too old' });
}
const expected = 'sha256=' + crypto
.createHmac('sha256', HELPR_SECRET)
.update(timestamp + '.', 'ascii')
.update(req.rawBody)
.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, os, time
from flask import Flask, request, jsonify
app = Flask(__name__)
HELPR_SECRET = os.environ['HELPR_DATA_CONNECTOR_SECRET']
def verify_helpr(f):
def wrapper(*args, **kwargs):
timestamp = request.headers.get('X-Helpr-Timestamp', '')
signature = request.headers.get('X-Helpr-Signature', '')
if (not timestamp or len(timestamp) > 12
or not timestamp.isascii() or not timestamp.isdecimal()
or timestamp[0] == '0' or len(signature) != 71
or not signature.startswith('sha256=')
or any(c not in '0123456789abcdef' for c in signature[7:])):
return jsonify(error='Missing or malformed signature'), 401
if abs(int(time.time()) - int(timestamp)) > 300:
return jsonify(error='Request too old'), 401
body = request.get_data(cache=True)
signed_payload = timestamp.encode('ascii') + b'.' + body
expected = 'sha256=' + hmac.new(
HELPR_SECRET.encode(), signed_payload, 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_DATA_CONNECTOR_SECRET'];
// Verify signature
$timestamp = $_SERVER['HTTP_X_HELPR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HELPR_SIGNATURE'] ?? '';
if (!preg_match('/^[1-9][0-9]*$/D', $timestamp)
|| !preg_match('/^sha256=[a-f0-9]{64}$/D', $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Missing or malformed signature']);
exit;
}
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
echo json_encode(['error' => 'Request too old']);
exit;
}
$rawBody = file_get_contents('php://input');
if ($rawBody === false) {
http_response_code(400);
echo json_encode(['error' => 'Could not read request body']);
exit;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $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_DATA_CONNECTOR_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']
unless timestamp&.match?(/\A[1-9][0-9]*\z/) &&
signature&.match?(/\Asha256=[a-f0-9]{64}\z/)
halt 401, { error: 'Missing or malformed signature' }.to_json
end
if (Time.now.to_i - timestamp.to_i).abs > 300
halt 401, { error: 'Request too old' }.to_json
end
signed_payload = timestamp.b + '.'.b + @raw_body.b
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', HELPR_SECRET, signed_payload)
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.