API Reference
The HorizonLink Proximity Verification API lets your backend verify that a HorizonLink user is physically present within a defined radius of a target location. Requests are answered in-app, scored against hardware sensor data and server signals, and delivered to your webhook.
AUTHENTICATION
All API requests require your API key in the X-API-Key header.
X-API-Key: hlv_your_api_key_hereKeys are generated from your dashboard or the HorizonLink app (Settings → API Access). Each key is shown once at creation — store it in an environment variable.
BASE URL
https://gjxpulfkxhpoghcsklsx.supabase.co/functions/v1ERROR HANDLING
All errors return JSON with an error field and an appropriate HTTP status code.
{ "error": "Missing required fields" }| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid fields |
| 401 | Invalid or missing API key |
| 404 | Target user not found |
| 409 | User already has a pending request |
| 429 | Monthly request limit reached (free plan: 100/mo) |
| 500 | Internal server error |
POSTCREATE PROXIMITY REQUEST
POST https://gjxpulfkxhpoghcsklsx.supabase.co/functions/v1/v1-proximity-createCreates a new proximity verification request. The target user receives a push notification in the HorizonLink app and can approve or decline.
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| target_user_id | string (UUID) | Yes | The HorizonLink user's UUID. Obtainable from the user via deep link or your own auth flow. |
| radius_m | number | Yes | Radius in metres. The verification passes only if the user is within this distance. |
| min_trust_score | number | No | If set, the webhook meets_min_score field will reflect whether the score meets this threshold (0–100). |
| webhook_url | string (URL) | No | Override the default webhook URL for this specific request. |
| expires_in_sec | number | No | How long (seconds) the request stays open. Default: 300 (5 min). Max: 3600. |
Example request
curl -X POST https://gjxpulfkxhpoghcsklsx.supabase.co/functions/v1/v1-proximity-create \
-H "X-API-Key: hlv_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"target_user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"radius_m": 100,
"min_trust_score": 70
}'Success response — 200
{
"request_id": "uuid-of-the-created-request",
"status": "pending",
"expires_at": "2026-05-07T12:35:00.000Z"
}GETPOLL REQUEST STATUS
GET https://gjxpulfkxhpoghcsklsx.supabase.co/functions/v1/v1-proximity-status?request_id={uuid}Retrieves the current status and result of a proximity request. Poll this endpoint until status is no longer pending, or use webhooks for push delivery.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
| request_id | string (UUID) | Yes | The request ID returned by the create endpoint. |
Example request
curl "https://gjxpulfkxhpoghcsklsx.supabase.co/functions/v1/v1-proximity-status?request_id=uuid-here" \
-H "X-API-Key: hlv_your_api_key_here"Success response — 200
{
"request_id": "uuid",
"status": "approved", // pending | approved | out_of_range | declined | expired
"trust_score": 85, // 0–100, null while pending
"trust_flags": ["near_boundary"], // array of failed signal types
"distance_m": 47, // null while pending
"meets_min_score": true, // null if min_trust_score not set
"created_at": "2026-05-07T12:30:00.000Z",
"expires_at": "2026-05-07T12:35:00.000Z"
}Status values
| Status | Meaning |
|---|---|
| pending | Request sent, waiting for user to respond |
| approved | User responded and was within the radius |
| out_of_range | User responded but was outside the radius |
| declined | User explicitly declined the request |
| expired | User did not respond before expires_at |
WEBHOOK DELIVERY
When a request is resolved, HorizonLink POSTs a signed JSON payload to your webhook URL. Webhooks are attempted up to 3 times with exponential backoff (1s → 2s) on failure. Your endpoint must return a 2xx status within 5 seconds.
v1-proximity-status) for time-critical flows.SIGNATURE VERIFICATION
Every webhook request includes an X-HL-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.
Your webhook secret is set during registration (or can be updated in your dashboard). Verify every webhook to prevent replay attacks.
Verification — Node.js example
import crypto from 'crypto';
function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your webhook handler:
const sig = req.headers['x-hl-signature'];
const raw = req.rawBody; // unparsed request body string
if (!verifyWebhook(raw, sig, process.env.HL_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}WEBHOOK PAYLOAD REFERENCE
{
"event": "proximity.result",
"request_id": "uuid",
"status": "approved",
"trust_score": 85,
"distance_m": 47,
"meets_min_score": true,
"verified_at": "2026-05-07T12:31:44.000Z",
"flags": []
}| Field | Type | Description |
|---|---|---|
| event | string | Always "proximity.result" |
| request_id | string | UUID of the request |
| status | string | Final status (see status values above) |
| trust_score | number | Composite trust score 0–100 |
| distance_m | number | Distance in metres from the target location |
| meets_min_score | boolean|null | Whether score ≥ min_trust_score. null if not set. |
| verified_at | string | ISO 8601 timestamp of when the result was recorded |
| flags | string[] | Array of failed signal type names |
HOW THE TRUST SCORE WORKS
The trust score (0–100) is a composite measure computed server-side from both server-verifiable signals and client-reported hardware sensor readings. Starting at 100, deductions are applied for each anomaly.
Hard failures (mock location provider, out-of-range distance) immediately set the score to 0 and abort further checks.
SIGNAL REFERENCE
| Signal | Source | Impact | Description |
|---|---|---|---|
| mock_provider | Client / OS | Hard fail (−100) | OS reports mock location provider active |
| out_of_range | Server | Hard fail (−100) | Haversine distance > radius_m |
| near_boundary | Server | −10 | Distance > 75% of radius |
| gps_accuracy | Server | 0 / −20 / −35 | ±30m = 0, ±60m = −20, >60m = −35 |
| speed | Server | 0 / −20 / −50 | Normal = 0, >54km/h = −20, >180km/h = −50 |
| response_time | Server | −10 / 0 / +5 | >45s = −10, 15–45s = 0, <15s = +5 |
| ip_gps_country | Server | 0 / −20 | IP country ≠ GPS country = −20 |
| accel_variance | Client | −25 | Low accelerometer variance (device stationary / spoofed) |
| baro_altitude | Client | −15 | Barometer altitude disagrees with GPS altitude by >120m |
| speed_sanity | Client | −25 | Consecutive GPS fixes imply >900 km/h travel |