LogirAI Webhooks β API Reference (v1) β
This guide describes the current contract for webhooks LogirAI sends to your system. If a behavior is not described here, assume it is not supported in v1.
1. Overview β
When events occur in the logistics operation, LogirAI sends HTTP POST notifications to the URL you have registered with us.
- Fire-and-forget: 1 event β 1
POST. No retries. - Authentication:
Authorization: Bearer <secret>header. - No per-event filtering in v1: your URL receives every event available for your company.
- One active URL per client in v1. If you need multiple destinations, contact operations.
2. Onboarding β
Webhook setup and teardown are handled manually with the LogirAI team (no self-service in v1).
To enable webhooks, send us:
- Destination URL β must be
https://...(plain HTTP is rejected). - Technical contact β to coordinate the secret.
LogirAI will return a secret that will arrive in every request as the Authorization: Bearer <secret> header. Store it securely.
To rotate the secret or disable the subscription: contact operations. Rotation is immediate (no grace period: the next event uses the new secret).
3. Request structure β
Method and URL β
POST <your-registered-url>Headers β
| Header | Value |
|---|---|
Authorization | Bearer <secret> |
Content-Type | application/json |
User-Agent | LogirAI-Webhook/1.0 |
X-Logir-Delivery-Id | Unique delivery UUID (format evt_<uuid-v4>) |
X-Logir-Event | Event type (e.g. guide.delivered) |
We do not send an HMAC signature. Authentication is exclusively the
Bearertoken.
Timeout β
LogirAI waits up to 10 seconds for a response. After that, the request is aborted and the event is considered failed (no retry).
Body β envelope β
All events share this outer structure:
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"type": "guide.delivered",
"version": "1.0",
"occurred_at": "2026-04-28T14:32:11.123Z",
"company": {
"code": "ACME",
"country": "CL"
},
"data": { ... }
}| Field | Type | Description |
|---|---|---|
id | string | Unique delivery identifier. Matches the X-Logir-Delivery-Id header. Use it to deduplicate. |
type | string | One of the catalog values (Β§5). |
version | string | "1.0" in v1. The version is bumped when the shape changes. |
occurred_at | ISO 8601 (UTC) | Timestamp when the envelope was built. For the business event timestamp, see the field inside data (e.g. delivered_at). |
company.code | string | Your company code. May be empty ("") in rare degraded cases. |
company.country | string | "CL" or "AR". May be empty in the same case. |
data | object | Event-specific payload. See Β§5. |
4. What your system must do β
Expected response β
Return 2xx quickly. LogirAI does not inspect the response body β only the status code (logged for audit).
Your endpoint should:
- Validate the
Authorizationheader against your secret using a timing-safe comparison. - Persist or enqueue the event.
- Return 2xx (ideally in under 1 s).
Do not run heavy processing inline β receive, persist, return 200. Long-running tasks belong in a queue.
Idempotency β
Deduplicate by envelope.id (or the X-Logir-Delivery-Id header).
- Under normal operation, we do not send duplicates.
- The
guide.deliveredevent has explicit single-emit guarantees on LogirAI's side. - Other events may duplicate in edge cases (e.g. an operator re-runs an action). Your client must be tolerant of duplicates.
Authentication β
Timing-safe comparison of the secret. Example in Node.js:
const crypto = require('crypto');
function isAuthorized(req, expectedSecret) {
const header = req.headers.authorization || '';
const m = header.match(/^Bearer\s+(.+)$/);
if (!m) return false;
const a = Buffer.from(m[1]);
const b = Buffer.from(expectedSecret);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Never include the secret in logs or responses.
Order and latency β
- Order is not guaranteed between events. If your logic depends on order, use
occurred_ator the timestamps insidedata. - No per-destination rate limit: if LogirAI emits many events in a row, they arrive in parallel (bounded only by our internal concurrency).
5. Event catalog β
The catalog is closed at 18 types, all active.
type | v1 status | Description |
|---|---|---|
mawb.created | β Active | Pre-alert processed; MAWB and guides created in LogirAI. |
mawb.customs_process_started | β Active | Customs file generated (AIDA in AR, ATREX in AR). |
mawb.flight_arrived | β Active | The flight arrived at the destination airport (ATA confirmed). |
mawb.incident.created | β Active | An incident was recorded against a MAWB. |
mawb.incident.resolved | β Active | An incident on a MAWB was resolved. |
guide.presumed_retention | β Active | MAWB closed with HAWBs missing from scan β presumed retained. |
guide.held_at_customs | β Active | Customs has held the guide. |
guide.customs_released | β Active | Customs released the guide (whether or not it had previous holds). |
guide.picked_up_by_courier | β Active | The last-mile courier picked up the guide from the CD. |
guide.delivered | β Active | Delivery confirmed (PoD from operator or courier). |
guide.critical_annotation_added | β Active | A critical annotation was recorded against the guide (any type that is blocking for dispatch: customs hold, presumed abandonment, missing documentation, etc.). |
guide.scanned_in_customs | β Active | The guide was scanned on arrival at the customs warehouse. Emitted in batches, one event per guide. |
dispatch.cancelled | β Active | A dispatch (hoja de ruta) was cancelled and its guides reverted. One event per company on the dispatch. |
guide.damaged | β Active | Physical damage was recorded against the guide's cargo. |
guide.requires_documentation | β Active | The guide needs additional documentation before it can be released (ISP, SAG, SEREMI, DGMN, DIFROL, SIPRO, GICONA or SERNAPESCA permits). |
guide.arrived_at_destination_cd | β Active | The guide reached the destination distribution centre. |
guide.assigned_to_courier | β Active | The guide was assigned to a last-mile courier. |
guide.scanned_by_courier | β Active | The courier scanned the guide along its route. |
Per-event payloads (data field) β
mawb.created β
{
"mawb": "020-12345678",
"flight_number": "LA800",
"origin_airport": "MIA",
"destination_airport": "SCL",
"scheduled_arrival": "2026-04-29T08:00:00Z",
"guides_count": 47
}flight_number, origin_airport, destination_airport, scheduled_arrival may be null if the pre-alert did not include them.
guide.scanned_in_customs β
{
"guide_number": "DRL-2026-00001",
"mawb": "020-12345678",
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scanned_at": "2026-04-29T14:03:11Z",
"country": "CL"
}dispatch.cancelled β
{
"dispatch_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
"dispatch_number": "HR-2026-0042",
"cancelled_at": "2026-04-30T18:20:00Z",
"cancelled_by_user_id": "u1234567-89ab-cdef-0123-456789abcdef",
"reason": "Truck failed inspection",
"total_guides": 2,
"guides": [
{ "guide_id": "g1", "hawb": "DRL-2026-00001", "manifest_id": "m1", "reverted_to_status_code": 2 }
],
"mawb_ids_affected": ["m1"],
"batches_reopened": 1,
"bigboxes_reopened": 0
}One event per company represented on the dispatch: guides only ever carries that company's guides.
mawb.customs_process_started β
{
"mawb": "020-12345678",
"process_id": "uuid-of-the-manifest",
"txt_url": null,
"started_at": "2026-04-28T14:32:11.123Z"
}
txt_urlis alwaysnullin v1: the customs file (AIDA/ATREX) is not published to a public URL.
mawb.flight_arrived β
{
"mawb": "020-12345678",
"ata": "2026-04-29T08:14:00Z",
"destination_airport": "SCL",
"guides_count": 47
}mawb.incident.created β
Fires synchronously when the HTTP request that creates the incident completes (within the same request cycle, before the response is returned to the client).
data fields:
| Field | Type | Description |
|---|---|---|
manifest_id | UUID | Internal MAWB ID in LogirAI. |
mawb_number | string | Master guide number (e.g. 906-12345678). |
incidence_id | bigint | Unique incident ID. Use as the dedup key. |
type_code | string | Closed code for the incident type (see values below). |
type_label | string | Human-readable description of the type, in Spanish. |
comment | string | null | Free-form operator comment. May be null. |
created_by | UUID | ID of the user who recorded the incident. |
created_at | ISO 8601 (UTC) | Creation timestamp. |
is_blocking | boolean | true if the incident type blocks MAWB dispatch. |
files | array | Attached files. May be []. See detail below. |
type_code β possible values (12 closed values):
CARGO_NOT_ARRIVED, DAMAGED_BOXES_AND_LABELS, MISSING_PIECES, EXCESS_PIECES, WEIGHT_DISCREPANCY, DOCUMENTATION_ISSUE, CUSTOMS_HOLD, PROHIBITED_ITEM, REPACKAGING_REQUIRED, TEMPERATURE_EXCURSION, SECURITY_INCIDENT, OTHER
files[] β each element:
| Field | Type | Description |
|---|---|---|
url | string | Signed URL with a 1-hour TTL. Download immediately; the URL expires. |
name | string | Original file name. |
{
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mawb_number": "906-12345678",
"incidence_id": 4821,
"type_code": "DAMAGED_BOXES_AND_LABELS",
"type_label": "Cajas y etiquetas daΓ±adas",
"comment": "Pallet 3 arrived with signs of moisture. 4 boxes have illegible labels.",
"created_by": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"created_at": "2026-05-01T14:32:11.123Z",
"is_blocking": false,
"files": [
{
"url": "https://storage.logir.ai/signed/pallet-3-photo.jpg?token=abc123&expires=1746364331",
"name": "pallet-3-photo.jpg"
}
]
}Operational notes:
- Synchronous emission: if your endpoint is down, the event is lost (no retries β see Β§6).
- Each emission produces 1 row in
manifest_incidence_notification_logwith statussentorfailed. - In parallel, LogirAI sends a notification email via Resend to the recipients configured on the company β independent of the webhook.
files[].urlURLs expire in 1 hour. If you need to persist the files, download them when you receive the event.
mawb.incident.resolved β
Fires synchronously when the HTTP request that resolves the incident completes.
data fields:
| Field | Type | Description |
|---|---|---|
manifest_id | UUID | Internal MAWB ID in LogirAI. |
mawb_number | string | Master guide number. |
incidence_id | bigint | ID of the resolved incident. Correlate with the incidence_id from mawb.incident.created. |
type_code | string | Code of the original incident type. |
type_label | string | Human-readable description of the type, in Spanish. |
resolved_by | UUID | ID of the user who marked the incident as resolved. |
resolved_at | ISO 8601 (UTC) | Resolution timestamp. |
resolution_notes | string | null | Closing notes entered by the operator. May be null. |
is_blocking | boolean | Reflects whether the incident type was blocking. Useful to know whether dispatch can resume. |
{
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mawb_number": "906-12345678",
"incidence_id": 4821,
"type_code": "DAMAGED_BOXES_AND_LABELS",
"type_label": "Cajas y etiquetas daΓ±adas",
"resolved_by": "c89de012-34ab-5678-cdef-901234567890",
"resolved_at": "2026-05-01T17:15:42.000Z",
"resolution_notes": "The 4 affected boxes were re-labeled. Cargo is fit for dispatch.",
"is_blocking": false
}Operational notes:
- Synchronous emission within the resolution request (same pattern as
mawb.incident.created). - Produces 1 row in
manifest_incidence_notification_log. - Notification email via Resend in parallel, independent of the webhook.
- No file attachments on the resolution event.
guide.presumed_retention β
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"html_url": null,
"presumed_at": "2026-04-28T14:32:11.123Z"
}
html_urlis alwaysnullin v1.
guide.held_at_customs β
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"hold_reason": "D",
"hold_detail": "Missing DUI",
"held_at": "2026-04-28T14:32:11.123Z"
}hold_reason: canonical customs code (short string, e.g.D,R,DF).hold_detail: free-text reason written by the operator. May benull.
guide.customs_released β
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"released_at": "2026-04-28T14:32:11.123Z",
"had_previous_holds": true
}had_previous_holds: true if the guide was held before being released.
guide.picked_up_by_courier β
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"picked_up_at": "2026-04-28T14:32:11.123Z"
}guide.delivered β
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"delivered_at": "2026-04-28T14:32:11.123Z",
"pod_signatory": "Juan PΓ©rez",
"pod_image_url": "https://storage.../pod.jpg"
}pod_signatory and pod_image_url may be null (some couriers do not return signature or photo).
guide.critical_annotation_added β
Fires fire-and-forget (does not block the HTTP request that creates the annotation) every time a guide receives an annotation of a type marked as blocking for dispatch (annotation_types.is_blocking_for_dispatch = true). Covers all 4 internal annotation creation paths: /api/annotations endpoint (single create), /api/annotations/bulk endpoint (bulk create β emits once per guide), the abandonment-presumption cron, and legacy annotations generated by customs actions (hold/release).
data fields:
| Field | Type | Description |
|---|---|---|
guide_id | UUID | Internal LogirAI guide ID. |
annotation_id | UUID | Unique annotation ID. Use as the dedup key at the annotation level. |
type_code | string | Annotation type code (see critical types below). |
hold_id | UUID | ID of the hold associated with the annotation. Critical annotations always create a hold. |
{
"guide_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"annotation_id": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"type_code": "withheld_physical",
"hold_id": "11223344-5566-7788-99aa-bbccddeeff00"
}type_code β critical types currently active:
withheld_physical, withheld_documentary_declaration, withheld_documentary_customs, withheld_unpaid_taxes, sag_import_permit, required_documentation, presumption_abandonment, customs_retained_legacy
Operational notes:
- Co-emission with customs events: when a critical annotation originates from a customs action (hold or release), LogirAI emits two different events for the same physical operation: the domain event (
guide.held_at_customsorguide.customs_released) andguide.critical_annotation_added. They are distinct events, not duplicates. If you only care about one, filter bytypeon your side. - Bulk: a bulk create affecting N guides emits N events of
guide.critical_annotation_added(one per guide), not a single aggregate event. - Idempotency: retrying an annotation create with the same
Idempotency-Keywhen the annotation already exists (replay) does not re-emit the event. - Fire-and-forget: if the emitter fails (timeout, 5xx), the annotation is still persisted correctly β webhook emission does not block or break the business operation.
- Per-company routing: the event is delivered only to the webhook configured for the company that owns the guide. A critical annotation on a Lion360 guide goes to Lion360's webhook, not to other companies'.
6. Guarantees and limitations β
We guarantee β
https://is required for the destination URL.- Single-emit for
guide.delivered: even if two internal paths (operator scan + courier webhook) mark delivery concurrently, only one event is emitted. - Internal audit of every attempt (success or failure) for support and debugging.
We do NOT guarantee β
- No retries. A failure (timeout, 5xx, 4xx, connection refused) is final.
- No HMAC signature. Authentication is exclusively the
Bearersecret. - No ordering guarantee between events.
- No exactly-once delivery except for
guide.delivered. Your client must deduplicate byid. - No backpressure: if your endpoint is down,
POSTs are still issued and time out.
7. Recommendations β
- Return 2xx fast (< 1 s). Heavy processing belongs in a queue.
- Always deduplicate by
envelope.id. - Validate
Authorizationwith a timing-safe compare. - Tolerate
nullfields in payloads β we mark them as nullable when applicable. - Tolerate extra fields: if we add fields to
datain a v1.x release, we do not bumpversion. Version bumps are reserved for breaking changes. - Monitor your endpoint: if your URL returns 5xx for a long window, you will lose events (we do not retry them).
- Request a manual replay from operations if you detect a gap. We have a full audit trail of every attempt.