API Documentation

LOCURA API

v1.0

A 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 endpoints

Identity provider, clients, files, API keys, webhooks

https://hub.locura.tech/api/v1/

Pronostico

15 endpoints

Inventory management, ABC/XYZ, demand forecasting

https://pronostico.locura.tech/api/v1/

Avisator

16 endpoints

Dock scheduling, bookings, supplier communication

https://avisator.locura.tech/api/v1/

DocMaker

6 endpoints

AI 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

PrefixTypeDescription
lk_live_*Live SecretFull access to production data. Keep server-side only.
lk_test_*Test SecretSandbox environment. Safe for development and testing.
lk_pub_*PublishableRead-only access. Safe for client-side use.

Getting Your API Key

  1. Log into Hub at hub.locura.tech
  2. Navigate to Settings → API → Klucze API
  3. Click "Create new key", select scopes, and copy the key
  4. 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:

bash
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.

TierRequests / minBurst
Free6010 requests
Pro30050 requests
Enterprise1,000100 requests

Response Headers

Every response includes rate limit information:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when the window resets

429 Too Many Requests

json
{
  "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

json
{
  "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

StatusTitleDescription
400Bad RequestThe request body or parameters are malformed.
401UnauthorizedMissing or invalid API key.
403ForbiddenAPI key lacks the required scope for this operation.
404Not FoundThe requested resource does not exist.
409ConflictResource already exists or state conflict (e.g., double booking).
422Unprocessable EntityValidation error -- the request body failed schema validation.
429Too Many RequestsRate limit exceeded. Retry after the time indicated in the response.
500Internal Server ErrorUnexpected server error. Contact support with the request_id.

Pagination

All list endpoints support offset-based pagination with the following query parameters.

ParameterDefaultDescription
page1Page number (1-based)
per_page50Items per page (max 100)

Example Request

bash
curl "https://hub.locura.tech/api/v1/clients?page=2&per_page=25" \
  -H "Authorization: Bearer lk_live_abc123def456"

Response Format

json
{
  "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

EventAppDescription
avisator.booking.createdAvisatorA new booking was created
avisator.booking.approvedAvisatorA booking was approved
avisator.booking.rejectedAvisatorA booking was rejected
avisator.booking.cancelledAvisatorA booking was cancelled
avisator.booking.arrivedAvisatorVehicle arrived at the dock
avisator.booking.completedAvisatorBooking completed (loading/unloading done)
avisator.booking.no_showAvisatorVehicle did not show up
pronostico.analysis.completedPronosticoAnalysis job finished processing

Webhook Payload Format

json
{
  "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
javascript
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
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}), 200

Retry Policy

Failed deliveries (non-2xx response or timeout) are retried up to 6 times with exponential backoff:

AttemptDelay
1st retry2 minutes
2nd retry4 minutes
3rd retry8 minutes
4th retry16 minutes
5th retry32 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
bash
# 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
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
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
bash
# 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
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
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
bash
# 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
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
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
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

v1.0.02026-03-24

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