Fronteam API

Connect your ERP, CRM or booking system to the Fronteam Logistics calendar. Your system creates, updates and cancels events and gets every change made in Fronteam back (webhooks or polling); anything that would affect assigned crew waits for a manager's decision, and the API never sends a message to employees.

REST + JSONBase URL https://fline.app/api/v1 Version 1.0Enabled per company by the Fronteam admin

Getting started

  1. A manager of the company opens External system connection in Fronteam Logistics, creates a connection and maps your status codes (for example CONFIRMED) to their statuses.
  2. They create a test key (ftk_test_…) and turn the connection on, with Receive changes from their system checked. While the connection is off, every request is rejected (even with a test key); without receiving changes you can only read. With a test key every write is a dry run: the response tells what would happen, and nothing changes.
  3. They send you the access details. When your integration works, they create a live key (ftk_live_…).
  4. To get the changes made in Fronteam, they enter your webhook URL and check Send changes to their system; the signing secret (whsec_…) is shown to them once. Without a webhook you can poll GET /changes. See Changes from Fronteam.

The machine-readable spec: https://fline.app/api/v1/openapi.json (OpenAPI 3.1, no key needed).

Do not send prices in notes or description: the crew assigned to the event can read them.

Authentication

Authorization: Bearer ftk_live_0123…cdef

If your tool cannot send a Bearer header, send X-Api-Key: ftk_live_… instead. A key is shown to the manager only once; Fronteam keeps only a fingerprint of it. A revoked or expired key gets 401 right away.

Keys can be read-only (events:read) or read and write (events:write). Only https://fline.app/api/v1 is accepted - requests sent anywhere else get 403 proxy_required.

A key (and the webhook address) exposes all events with their customer details, so only a manager who may view events and customers can create one; a key that writes also needs permission to edit them.

Requests

  • Bodies are JSON (Content-Type: application/json), up to 256 KB (batch: 2 MB).
  • Idempotency: send Idempotency-Key on writes. A retry with the same key and body returns the first answer (Idempotency-Replayed: true) for 24 hours; the same key with a different body is 422 idempotency_key_reused.
  • Concurrency: every event has an ETag. Send If-Match to update only the version you read; otherwise 409 revision_mismatch. The revision also changes when Fronteam-only fields change - on 409, GET the event and try again.
  • Rate limits: per connection (default 120 requests per minute; batch up to 100 events). Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; over the limit you get 429 with Retry-After.
  • Dates are YYYY-MM-DD and times HH:MM, in the company's time zone; timestamps are UTC (ISO 8601). A field you do not send is not touched; null clears it.
  • Every answer has X-Request-Id - quote it when you contact support.

Endpoints

MethodPathWhat it does
GET/meThe connection, company, key and limits.
GET/statusesThe company's statuses, the code we send for each and the codes you may send.
GET/eventsEvents page by page: updated_since, from, to, status, include_cancelled, include_deleted, limit (up to 200), cursor.
GET/events/{external_id}An event by your id.
GET/events/by-fronteam-id/{id}An event by our id.
PUT/events/{external_id}Create (first time) or update (merge). ?dry_run=true = only tell what would happen.
PATCH/events/{external_id}Update an event you already linked (404 if not).
DELETE/events/{external_id}Cancel the event (it is never deleted).
POST/events/batchUp to 100 upserts / patches / cancels; each item succeeds or fails on its own.
DELETE/events/{external_id}/linkForget the link between your id and the event (the event stays).
GET/changesWhat changed in Fronteam since your cursor - the latest change of each event, oldest first.
GET/openapi.jsonThis API as OpenAPI 3.1.

Your external_id: letters, digits and . _ : ~ -, up to 128 characters. To link an event that already exists in Fronteam, send its fronteam_id in the first PUT (only when the connection covers all company events; otherwise an unlinked event is "not found" for you).

The event object

The same shape in every GET and in every write answer:

{
  "external_id": "ORD-1001",
  "fronteam_id": "7c1f…",
  "revision": 4,
  "name": "Product launch",
  "description": null,
  "notes": "Load-in through gate 3",
  "status": { "code": "CONFIRMED", "name": "Confirmed", "cancelled": false },
  "dates": { "start": "2026-11-05", "end": "2026-11-06" },
  "times": { "setup": "08:00", "start": "18:00", "end": "23:30" },
  "venue": { "name": "Expo Hall 2", "address": "Rokach Blvd 101", "contact_name": "Dana", "contact_phone": "+972501234567" },
  "customer": { "name": "Acme Ltd", "tax_id": "514000000", "phone": null, "email": "ops@acme.com",
                "fronteam_id": "0b9e…" },
  "contact": { "name": "Noa", "phone": "+972521112222", "email": "noa@acme.com" },
  "sales_agent": { "code": "AG-12", "name": "Avi" },
  "schedule": [ { "name": "Setup", "date": "2026-11-05", "start": "08:00", "end": "16:00" } ],
  "sync": { "state": "synced", "pending": [] },
  "deleted": false,
  "updated_at": "2026-10-27T08:15:00Z"
}

customer: matched by tax id, then name, phone and email; a new customer is created only if the company allows it. Existing customers are never changed by the API. schedule is used when the event is created; a later change becomes a note for the manager. sales_agent is the code on the agent's employee card.

Writes and decisions

StatusresultMeaning
201createdA new event.
200updated / unchangedApplied, or nothing changed (no new revision).
202pending_review / partially_appliedSomething waits for a manager - see held.
Crew first. Cancelling, restoring or moving the dates of an event with assigned crew - or with invitations, scheduled messages or approval requests about it still pending, or (for cancelling) equipment out on the job - is never applied by the API. You get 202 with the first reason that applies (crew_assigned / pending_invites / scheduled_messages / pending_approvals / equipment_out / conflicts) and the numbers, the manager sees it on the event, and the event updates when they decide - sync.pending shows what still waits. Venue or time changes are applied and the manager is asked whether to notify the crew. The API itself never sends a message to employees.
HTTP/1.1 202 Accepted
{
  "result": "pending_review",
  "applied_fields": [],
  "held": [ { "review_id": "3f2a…", "kind": "cancel_impact", "fields": ["status"], "reason": "crew_assigned",
              "impact": { "assigned": 5 } } ],
  "conflicts": [],
  "warnings": [],
  "event": { "external_id": "ORD-1001", "status": { "code": "CONFIRMED" }, "sync": { "state": "pending_review" } }
}

Both sides changed the same field: by default the value set in Fronteam wins and the conflict is reported to the manager (conflicts); the company can choose "theirs wins" or "ask me" instead.

Direction per field: for each field the company can choose both ways (the default), only from you, only from Fronteam, or not synced. A field that is "only from Fronteam" or "not synced" is not taken from your writes - the response says so in warnings (example below) and the rest of the request is applied; on create, name and dates.start are always taken. A field that is "only from you" or "not synced" is left out of everything we send you (GET, webhooks, /changes) - so a missing field means "not sent", never "cleared". Cancelling (DELETE) always goes through, whatever the direction of status.

"warnings": [ { "code": "field_not_writable", "field": "notes" } ]

Changes from Fronteam

When a manager edits, moves, cancels or deletes a linked event in Fronteam, your system hears about it - by webhook (we call you) or by polling GET /changes (you ask us). A change you made through the API is never sent back to you.

Webhooks

POST https://erp.example.com/fronteam/hook
webhook-id: 6d1c2b0e-…
webhook-timestamp: 1761552900
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{ "type": "event.updated", "timestamp": "2026-10-27T08:15:00Z",
  "data": { "sequence": 1851, "event": { "external_id": "ORD-1001", "revision": 5, … } } }
typeWhen
event.created / event.updatedA linked event was created or changed in Fronteam (name, dates, times, venue, status, customer, notes, schedule…).
event.cancelled / event.uncancelledMoved to / out of a cancelled status.
event.deleted / event.restoredDeleted (you get a tombstone with recoverable_until) / restored from the recycle bin.
change.rejectedPart of your write was not applied: Fronteam kept its own value in a conflict, or a manager declined a change that waited for a decision (data.held[].decision). data.conflicts / data.held as in the write answer, data.request_id = your request's X-Request-Id. Webhook only (not in GET /changes).
pingThe manager pressed "Send test".
  • data.event is always the event's current state, so several quick changes arrive as one delivery. If two deliveries of the same event arrive out of order, keep the one with the higher data.sequence. Treat event.updated for an event you don't know as a create.
  • Editing a customer card alone doesn't send anything - the event's customer details go out with its next change.
  • Answer 2xx within 10 seconds. Anything else - including redirects, which are not followed - is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h, then dropped (the manager can send it again). webhook-id and the body are the same on every retry: use the id to ignore duplicates. If the event changes while a delivery waits for a retry, that delivery is replaced by a new one (a new webhook-id) with the current state.
  • Answer 410 Gone to stop the deliveries. After 72 hours of failures the webhook is turned off as well, and the manager is told.
  • Your URL must be https on port 443 and public (internal addresses are refused). We have no fixed outgoing IP address - verify the signature instead of an IP allow-list.
  • Which events: only events linked to you, unless the Fronteam admin chose "all company events" - then you also get events you never sent (external_id: null), and that is the only case where event.created arrives (an event you create through the API is already yours).

Checking the signature

We follow Standard Webhooks - you can use their library for your language, or check it yourself:

signature = base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"))

where key is the base64-decoded part of your secret after whsec_. While the manager rotates the secret, webhook-signature holds two signatures separated by a space - accept the request if one matches. Reject timestamps more than 5 minutes away.

import crypto from 'node:crypto';

// app.post('/fronteam/hook', express.raw({ type: 'application/json' }), (req, res) => …)
function verifyFronteam(req, secret) {
  const id = req.headers['webhook-id'];
  const ts = Number(req.headers['webhook-timestamp']);
  if (!id || !ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${req.body}`).digest('base64');
  return String(req.headers['webhook-signature'] || '').split(' ').some((s) => {
    const [v, sig = ''] = s.split(',');
    return v === 'v1' && sig.length === expected.length &&
           crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

Polling: GET /changes

GET /changes?cursor=1842
{ "changes": [ { "sequence": 1851, "type": "event.cancelled", "occurred_at": "2026-10-27T08:15:00Z",
                 "event": { "external_id": "ORD-1001", … } } ],
  "next_cursor": "1851", "has_more": false }

Keep next_cursor and send it next time (first call - without a cursor). has_more: true - call again right away. Events come in the order their changes were saved, each in its current state; a change saved late comes after what you already got - never skipped. Works only while sending is on (otherwise 403 outbound_disabled). Changes are kept 30 days: a cursor older than that, or from before sending was turned off and on again, gets 410 cursor_expired - read GET /events again and continue without a cursor.

Errors

{ "error": { "code": "validation_failed", "message": "…", "request_id": "…",
             "details": { "errors": [ { "field": "dates.end", "reason": "on or after dates.start, up to 366 days" } ] } } }
StatusCodes
400invalid_json invalid_request invalid_cursor unknown_status
401missing_api_key invalid_api_key
403insufficient_scope integration_disabled inbound_disabled outbound_disabled company_inactive ip_not_allowed proxy_required
404 / 405route_not_found event_not_found method_not_allowed
409revision_mismatch already_linked external_id_conflict idempotency_in_progress employee_on_option
410event_deleted cursor_expired
413 / 415payload_too_large unsupported_media_type
422validation_failed unknown_field idempotency_key_reused no_cancel_status
429rate_limited too_many_failed_auth
5xxinternal_error read_only_transaction not_implemented integrations_paused server_busy timeout - retry with the same Idempotency-Key (Retry-After when given).

Examples