LOCURA API
v1.0A unified REST API for integrating with the LOCURA ecosystem. Connect your ERP, WMS, or custom applications to manage clients, inventory forecasting, dock scheduling, and AI-powered document generation.
Hub
16 endpointsIdentity provider, clients, files, API keys, webhooks
https://hub.locura.tech/api/v1/Pronostico
15 endpointsInventory management, ABC/XYZ, demand forecasting
https://pronostico.locura.tech/api/v1/Avisator
16 endpointsDock scheduling, bookings, supplier communication
https://avisator.locura.tech/api/v1/DocMaker
6 endpointsAI document generation, analyses, reports, contracts
https://docmaker.locura.tech/api/v1/Base URLs
Hub: https://hub.locura.tech/api/v1
Pronostico: https://pronostico.locura.tech/api/v1
Avisator: https://avisator.locura.tech/api/v1
DocMaker: https://docmaker.locura.tech/api/v1
Authentication
All API requests require a Bearer token in the Authorization header. API keys are managed through the Hub application.
API Key Format
| Prefix | Type | Description |
|---|---|---|
| lk_live_* | Live Secret | Full access to production data. Keep server-side only. |
| lk_test_* | Test Secret | Sandbox environment. Safe for development and testing. |
| lk_pub_* | Publishable | Read-only access. Safe for client-side use. |
Getting Your API Key
- Log into Hub at
hub.locura.tech - Navigate to Settings → API → Klucze API
- Click "Create new key", select scopes, and copy the key
- Store the key securely -- it will not be shown again
Security: API keys are stored as SHA-256 hashes. Once created, the full key cannot be retrieved. If you lose your key, revoke it and create a new one.
Usage
Include the key as a Bearer token in every request:
curl -X GET "https://hub.locura.tech/api/v1/clients" \ -H "Authorization: Bearer lk_live_abc123def456ghi789jkl012mno345"
Rate Limiting
API requests are rate-limited per API key. The limits depend on your subscription tier.
| Tier | Requests / min | Burst |
|---|---|---|
| Free | 60 | 10 requests |
| Pro | 300 | 50 requests |
| Enterprise | 1,000 | 100 requests |
Response Headers
Every response includes rate limit information:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests per window |
| X-RateLimit-Remaining | Remaining requests in current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
429 Too Many Requests
{ "type": "https://api.locura.tech/errors/rate-limit-exceeded", "title": "Rate limit exceeded", "status": 429, "detail": "You have exceeded 60 requests per minute. Please retry after 23 seconds.", "instance": "/api/v1/clients", "request_id": "req_a1b2c3d4e5f6", "retry_after": 23 }
Brute Force Protection
After 5 consecutive failed authentication attempts, an exponential delay is applied. The delay doubles with each additional failure, starting at 1 second up to a maximum of 60 seconds.
Error Handling
All errors follow the RFC 7807 Problem Details format, providing consistent, machine-readable error responses.
Error Response Structure
{ "type": "https://api.locura.tech/errors/validation-error", "title": "Validation Error", "status": 422, "detail": "Field 'email' must be a valid email address.", "instance": "/api/v1/suppliers", "request_id": "req_x7y8z9a0b1c2" }
HTTP Status Codes
| Status | Title | Description |
|---|---|---|
| 400 | Bad Request | The request body or parameters are malformed. |
| 401 | Unauthorized | Missing or invalid API key. |
| 403 | Forbidden | API key lacks the required scope for this operation. |
| 404 | Not Found | The requested resource does not exist. |
| 409 | Conflict | Resource already exists or state conflict (e.g., double booking). |
| 422 | Unprocessable Entity | Validation error -- the request body failed schema validation. |
| 429 | Too Many Requests | Rate limit exceeded. Retry after the time indicated in the response. |
| 500 | Internal Server Error | Unexpected server error. Contact support with the request_id. |
Pagination
All list endpoints support offset-based pagination with the following query parameters.
| Parameter | Default | Description |
|---|---|---|
| page | 1 | Page number (1-based) |
| per_page | 50 | Items per page (max 100) |
Example Request
curl "https://hub.locura.tech/api/v1/clients?page=2&per_page=25" \ -H "Authorization: Bearer lk_live_abc123def456"
Response Format
{ "data": [ { "id": "cli_abc123", "name": "Acme Corp", "nip": "1234567890" } ], "pagination": { "page": 2, "per_page": 25, "total": 87 }, "links": { "next": "/api/v1/clients?page=3&per_page=25", "prev": "/api/v1/clients?page=1&per_page=25" } }
Hub API
The Hub is the central identity provider for the LOCURA ecosystem. It manages clients, files stored in Cloudflare R2, API keys, webhooks, usage tracking, and GDPR compliance.
Base URL: https://hub.locura.tech/api/v1
Health Check
Clients
Files (R2 Storage)
API Keys
Webhooks
Usage & Audit
GDPR Compliance
Sandbox
Changelog
Pronostico API
Pronostico is the inventory management and demand forecasting engine. Push your sales, stock, and purchasing data, then run AI-powered analysis to get ABC/XYZ classifications, demand forecasts, and purchase order recommendations.
Base URL: https://pronostico.locura.tech/api/v1
Health Check
Data Push
Push your operational data to Pronostico before running analysis. For datasets with 5,000 rows or fewer, processing is synchronous. Larger datasets return an async job ID.
Analysis
Results
Suppliers
Avisator API
Avisator handles dock scheduling and supplier communication. Manage warehouse locations, docks, time slots, and booking reservations for inbound and outbound deliveries.
Base URL: https://avisator.locura.tech/api/v1
Health Check
Locations
Bookings
Suppliers
Vehicles
Available Slots
Dock Map
DocMaker API
DocMaker is the AI-powered document generation platform. It produces analyses, audits, offers, contracts, presentations, articles, bonus systems, Gantt charts, transcriptions, and more. Documents are generated asynchronously and stored in R2.
Base URL: https://docmaker.locura.tech/api/v1
Health Check
Documents
Generation Jobs
Webhooks
Webhooks allow your application to receive real-time notifications when events occur in the LOCURA ecosystem. Events are delivered via HTTP POST to your configured endpoint with an HMAC-SHA256 signature for verification.
Available Events
| Event | App | Description |
|---|---|---|
| avisator.booking.created | Avisator | A new booking was created |
| avisator.booking.approved | Avisator | A booking was approved |
| avisator.booking.rejected | Avisator | A booking was rejected |
| avisator.booking.cancelled | Avisator | A booking was cancelled |
| avisator.booking.arrived | Avisator | Vehicle arrived at the dock |
| avisator.booking.completed | Avisator | Booking completed (loading/unloading done) |
| avisator.booking.no_show | Avisator | Vehicle did not show up |
| pronostico.analysis.completed | Pronostico | Analysis job finished processing |
Webhook Payload Format
{ "id": "evt_a1b2c3d4e5", "type": "avisator.booking.created", "timestamp": "2026-03-24T10:00:00.000Z", "data": { "booking_id": "bk_e5f6g7h8", "location_id": "loc_a1b2c3d4", "dock_id": "dock_x1y2z3", "supplier_id": "sup_m1n2o3p4", "date": "2026-03-25", "start_time": "08:00" } }
HMAC-SHA256 Signature Verification
Each webhook delivery includes an X-Locura-Signature header. Verify it by computing the HMAC-SHA256 of the raw request body using your webhook secret.
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your Express handler:
app.post('/webhooks/locura', (req, res) => {
const signature = req.headers['x-locura-signature'];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.LOCURA_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const event = req.body;
console.log('Received event:', event.type);
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook_signature(payload, signature, secret):
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/locura', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Locura-Signature')
is_valid = verify_webhook_signature(
request.get_data(as_text=True),
signature,
os.environ['LOCURA_WEBHOOK_SECRET']
)
if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
print(f"Received event: {event['type']}")
return jsonify({'received': True}), 200Retry Policy
Failed deliveries (non-2xx response or timeout) are retried up to 6 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 2 minutes |
| 2nd retry | 4 minutes |
| 3rd retry | 8 minutes |
| 4th retry | 16 minutes |
| 5th retry | 32 minutes |
| 6th retry (final) | 64 minutes |
After 6 failed attempts, the webhook is marked as disabled. You can re-enable it through the Hub UI or by deleting and recreating it via the API.
Code Examples
Complete working examples for common integration scenarios across all supported languages.
1. Authenticate and List Clients
curl
# List all clients curl "https://hub.locura.tech/api/v1/clients" \ -H "Authorization: Bearer lk_live_abc123def456ghi789jkl012mno345" # Search for a specific client curl "https://hub.locura.tech/api/v1/clients?search=Acme&per_page=5" \ -H "Authorization: Bearer lk_live_abc123def456ghi789jkl012mno345"
Python
import requests
API_KEY = "lk_live_abc123def456ghi789jkl012mno345"
BASE_URL = "https://hub.locura.tech/api/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# List clients
response = requests.get(f"{BASE_URL}/clients", headers=headers)
data = response.json()
for client in data["data"]:
print(f"{client['name']} (NIP: {client['nip']})")
print(f"Total: {data['pagination']['total']} clients")JavaScript
const API_KEY = 'lk_live_abc123def456ghi789jkl012mno345';
const BASE_URL = 'https://hub.locura.tech/api/v1';
async function listClients() {
const response = await fetch(`${BASE_URL}/clients`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
const { data, pagination } = await response.json();
data.forEach(client => {
console.log(`${client.name} (NIP: ${client.nip})`);
});
console.log(`Total: ${pagination.total} clients`);
}
listClients();2. Create a Dock Booking
curl
# Step 1: Check available slots
curl "https://avisator.locura.tech/api/v1/slots?location_id=loc_a1b2c3d4&date=2026-03-25" \
-H "Authorization: Bearer lk_live_abc123def456"
# Step 2: Create the booking
curl -X POST "https://avisator.locura.tech/api/v1/bookings" \
-H "Authorization: Bearer lk_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_a1b2c3d4",
"dock_id": "dock_x1y2z3",
"supplier_id": "sup_m1n2o3p4",
"vehicle_type_id": "vt_tir",
"date": "2026-03-25",
"start_time": "09:00",
"license_plate": "DW 12345",
"driver_name": "Jan Kowalski",
"cargo_description": "20 pallets of electronics"
}'Python
import requests
API_KEY = "lk_live_abc123def456ghi789jkl012mno345"
BASE_URL = "https://avisator.locura.tech/api/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# Check available slots
slots = requests.get(
f"{BASE_URL}/slots",
headers=headers,
params={"location_id": "loc_a1b2c3d4", "date": "2026-03-25"}
).json()
# Find first available slot
for dock in slots["data"]:
for slot in dock["slots"]:
if slot["available"]:
print(f"Available: {dock['dock_name']} at {slot['start_time']}")
break
# Create booking
booking = requests.post(
f"{BASE_URL}/bookings",
headers=headers,
json={
"location_id": "loc_a1b2c3d4",
"dock_id": "dock_x1y2z3",
"supplier_id": "sup_m1n2o3p4",
"vehicle_type_id": "vt_tir",
"date": "2026-03-25",
"start_time": "09:00",
"license_plate": "DW 12345",
"driver_name": "Jan Kowalski",
"cargo_description": "20 pallets of electronics"
}
).json()
print(f"Booking created: {booking['data']['id']}")JavaScript
const API_KEY = 'lk_live_abc123def456ghi789jkl012mno345';
const BASE_URL = 'https://avisator.locura.tech/api/v1';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
async function createBooking() {
// Check available slots
const slotsRes = await fetch(
`${BASE_URL}/slots?location_id=loc_a1b2c3d4&date=2026-03-25`,
{ headers }
);
const slots = await slotsRes.json();
// Create the booking
const bookingRes = await fetch(`${BASE_URL}/bookings`, {
method: 'POST',
headers,
body: JSON.stringify({
location_id: 'loc_a1b2c3d4',
dock_id: 'dock_x1y2z3',
supplier_id: 'sup_m1n2o3p4',
vehicle_type_id: 'vt_tir',
date: '2026-03-25',
start_time: '09:00',
license_plate: 'DW 12345',
driver_name: 'Jan Kowalski',
cargo_description: '20 pallets of electronics'
})
});
const booking = await bookingRes.json();
console.log(`Booking created: ${booking.data.id}`);
}
createBooking();3. Push Sales Data and Run Analysis
curl
# Step 1: Push sales data
curl -X POST "https://pronostico.locura.tech/api/v1/data/sales" \
-H "Authorization: Bearer lk_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{"sku": "SKU-001", "date": "2026-01-15", "quantity": 120, "revenue": 4560.00},
{"sku": "SKU-001", "date": "2026-02-15", "quantity": 135, "revenue": 5130.00},
{"sku": "SKU-002", "date": "2026-01-15", "quantity": 45, "revenue": 1350.00}
]
}'
# Step 2: Start analysis
curl -X POST "https://pronostico.locura.tech/api/v1/analysis/run" \
-H "Authorization: Bearer lk_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{"type": "full"}'
# Step 3: Check job status (poll until completed)
curl "https://pronostico.locura.tech/api/v1/analysis/job_q1r2s3t4" \
-H "Authorization: Bearer lk_live_abc123def456"
# Step 4: Get results
curl "https://pronostico.locura.tech/api/v1/analysis/job_q1r2s3t4/results" \
-H "Authorization: Bearer lk_live_abc123def456"Python
import requests
import time
API_KEY = "lk_live_abc123def456ghi789jkl012mno345"
BASE_URL = "https://pronostico.locura.tech/api/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# Step 1: Push sales data
upload = requests.post(
f"{BASE_URL}/data/sales",
headers=headers,
json={
"rows": [
{"sku": "SKU-001", "date": "2026-01-15", "quantity": 120, "revenue": 4560.00},
{"sku": "SKU-001", "date": "2026-02-15", "quantity": 135, "revenue": 5130.00},
{"sku": "SKU-002", "date": "2026-01-15", "quantity": 45, "revenue": 1350.00}
]
}
).json()
print(f"Upload: {upload['data']['upload_id']}")
# Step 2: Start analysis
analysis = requests.post(
f"{BASE_URL}/analysis/run",
headers=headers,
json={"type": "full"}
).json()
job_id = analysis["data"]["job_id"]
print(f"Analysis started: {job_id}")
# Step 3: Poll for completion
while True:
status = requests.get(
f"{BASE_URL}/analysis/{job_id}",
headers=headers
).json()
if status["data"]["status"] == "completed":
print("Analysis completed!")
break
elif status["data"]["status"] == "failed":
print("Analysis failed!")
break
print(f"Progress: {status['data']['progress']}%")
time.sleep(5)
# Step 4: Get results
results = requests.get(
f"{BASE_URL}/analysis/{job_id}/results",
headers=headers
).json()
summary = results["data"]["summary"]
print(f"Analyzed {summary['total_skus']} SKUs")
print(f"Recommendations: {summary['recommendations_count']}")JavaScript
const API_KEY = 'lk_live_abc123def456ghi789jkl012mno345';
const BASE_URL = 'https://pronostico.locura.tech/api/v1';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function runAnalysis() {
// Step 1: Push sales data
const upload = await fetch(`${BASE_URL}/data/sales`, {
method: 'POST',
headers,
body: JSON.stringify({
rows: [
{ sku: 'SKU-001', date: '2026-01-15', quantity: 120, revenue: 4560.00 },
{ sku: 'SKU-001', date: '2026-02-15', quantity: 135, revenue: 5130.00 },
{ sku: 'SKU-002', date: '2026-01-15', quantity: 45, revenue: 1350.00 }
]
})
}).then(r => r.json());
console.log(`Upload: ${upload.data.upload_id}`);
// Step 2: Start analysis
const analysis = await fetch(`${BASE_URL}/analysis/run`, {
method: 'POST',
headers,
body: JSON.stringify({ type: 'full' })
}).then(r => r.json());
const jobId = analysis.data.job_id;
console.log(`Analysis started: ${jobId}`);
// Step 3: Poll for completion
let status;
do {
await sleep(5000);
status = await fetch(`${BASE_URL}/analysis/${jobId}`, { headers })
.then(r => r.json());
console.log(`Progress: ${status.data.progress}%`);
} while (status.data.status !== 'completed' && status.data.status !== 'failed');
// Step 4: Get results
const results = await fetch(
`${BASE_URL}/analysis/${jobId}/results`,
{ headers }
).then(r => r.json());
const summary = results.data.summary;
console.log(`Analyzed ${summary.total_skus} SKUs`);
console.log(`Recommendations: ${summary.recommendations_count}`);
}
runAnalysis();4. Check Document Generation Status
Python
import requests
import time
API_KEY = "lk_live_abc123def456ghi789jkl012mno345"
BASE_URL = "https://docmaker.locura.tech/api/v1"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# List active generation jobs
jobs = requests.get(
f"{BASE_URL}/jobs",
headers=headers,
params={"status": "processing"}
).json()
for job in jobs["data"]:
print(f"Job {job['id']}: {job['document_type']} - {job['progress']}%")
# Wait for a specific job to complete
job_id = "gjob_e5f6g7h8"
while True:
job = requests.get(
f"{BASE_URL}/jobs/{job_id}",
headers=headers
).json()
status = job["data"]["status"]
progress = job["data"]["progress"]
step = job["data"].get("current_step", "")
print(f"[{progress}%] {step}")
if status == "completed":
doc_id = job["data"]["document_id"]
print(f"Document ready: {doc_id}")
# Download the document
response = requests.get(
f"{BASE_URL}/documents/{doc_id}/download",
headers=headers
)
with open("document.pdf", "wb") as f:
f.write(response.content)
print("Downloaded: document.pdf")
break
elif status == "failed":
print("Generation failed!")
break
time.sleep(3)Changelog
Initial Release
- REST API v1 with unified authentication across all LOCURA apps
- Hub API -- 16 endpoints: clients, files (R2), API keys, webhooks, usage, GDPR, sandbox
- Pronostico API -- 15 endpoints: data push (sales, stock, POs, prices), analysis, classifications, forecasts, recommendations, suppliers
- Avisator API -- 16 endpoints: locations, docks, bookings, suppliers, vehicles, slots, dock map
- DocMaker API -- 6 endpoints: documents, download, generation jobs
- Webhook system with HMAC-SHA256 verification and exponential retry backoff
- Rate limiting with 3 tiers (free: 60/min, pro: 300/min, enterprise: 1000/min)
- RFC 7807 Problem Details error format
- Sandbox environment with test keys (lk_test_*)
- Swagger UI available for all 4 applications