# Gaard — full public documentation > Concatenated English documentation for gaard.ai. Curated index: https://gaard.ai/llms.txt --- # Classification result URL: https://gaard.ai/docs/classification-result > Detailed classification result type, status constants, video details, and response examples. The classification result is returned by: - `POST /api/classify?sync=true` - `GET /api/result/{id}` - [Webhooks](/docs/webhooks/) when webhook delivery is configured ## Examples ### Missing API key (403) ```json { "success": false, "error": "no access to current endpoint, make sure you are using correct API key" } ``` ### Async response ```json { "id": "6662efdb5200108549e3ac2b", "accepted_at": "2024-06-07T13:32:43.811+02:00" } ``` ### Sync response ```json { "id": "6662f5c1f897618de43f0bbd", "status": { "classify": "accepted", "video": "accepted" }, "camera_id": "134188-VI08", "tenant": "test", "duration": 1434714, "duration_seconds": 1, "model": "noname", "version": "2.0.16123", "error_code": 0, "error_msg": "", "risk": "", "labels": ["intrusion"], "scores": { "person": 0.127, "vehicule": 0.893, "intrusion": 0.919, "flag": 0.830, "animal": 0.062, "plant": 0.049, "other": 0.020, "wind": 0.007, "rain": 0.005, "web": 0.004, "spider": 0.003, "text": 0.001, "NOTHING": 0.0003 }, "video": { "videoname": "video.mp4", "filesize": 246792, "specs": { "duration": 2.3, "fps": 2.61, "original.fps": 3, "original.height": 480, "original.width": 640, "width": 426, "height": 320, "nframes": 6, "original.nframes": 6 } }, "metadata": { "parc_origine": "XX", "client_id": 134188, "code_msg": "VI08", "no_trans": "20818" }, "created_at": "2024-06-07T13:57:55.230+02:00", "started_at": "2024-06-07T13:57:55.230+02:00" } ``` ## Basic information | Field | JSON | Description | | --- | --- | --- | | ID | `id` | Unique identifier of the result (`classify_id`). | | CameraID | `camera_id` | Camera identifier, derived from metadata. | | Tenant | `tenant` | Tenant identifier (mandatory). | | Model | `model` | Model used for classification (optional). | | Version | `version` | Model version (optional). | The `camera_id` is generated from metadata: ``` camera_id = metadata.client_id + metadata.camera_id camera_id = metadata.client_id + metadata.code_msg // fallback ``` ## Duration | Field | Type | JSON | Description | | --- | --- | --- | --- | | Duration | int64 | `duration` | Classification duration in milliseconds. | | DurationSeconds | int | `duration_seconds` | Classification duration in seconds. | ## Status and errors | Field | Type | JSON | Description | | --- | --- | --- | --- | | Status | object | `status` | Nested status for classify and video processing. | | ErrorCode | int | `error_code` | Error code (0 = success). | | ErrorMessage | string | `error_msg` | Detailed error message. | ### Status constants | Value | Description | | --- | --- | | `accepted` | The analysis has been accepted. | | `in-progress` | The analysis is currently in progress. | | `done` | The analysis is completed. | | `error` | An error occurred during the analysis. | | `timeout` | The analysis timed out. | ## Analysis results | Field | Type | JSON | Description | | --- | --- | --- | --- | | Risk | string | `risk` | Risk level: `safe`, `danger`, or `intrusion`. | | Labels | string[] | `labels` | Labels assigned by the analysis. | | Scores | object | `scores` | Confidence scores per label (0.0–1.0). | See [Risk, labels, and scores](/docs/risk-labels-scores/) for details on how scores determine risk levels. ## Video details | Field | Type | JSON | Description | | --- | --- | --- | --- | | Videoname | string | `videoname` | Name of the video file. | | Filesize | int | `filesize` | Video file size in bytes. | | VideoSpecs | object | `specs` | Video specifications (resolution, fps, frame count). | ## Timestamps | Field | Type | JSON | Description | | --- | --- | --- | --- | | CreatedAt | time | `created_at` | When the analysis was created. | | StartedAt | time | `started_at` | When the analysis started. | ## Metadata | Field | Type | JSON | Description | | --- | --- | --- | --- | | Metadata | object | `metadata` | Optional additional data. See [Metadata](/docs/metadata/). | --- # Endpoints URL: https://gaard.ai/docs/endpoints > Gaard API v3.0 endpoint reference: classify videos, retrieve results, download annotated video, and delete results. ## Overview ' }, { method: 'DELETE', path: '/api/classify/' }, { method: 'GET', path: '/api/video/annotate/' }, ]} /> ## Send video to classification By default, classification is **asynchronous** (`sync=false`). Set `sync=true` to wait for the result in the response. ### Async response ```json { "id": "66436fc66d24ab9cf81140eb", "accepted_at": "2024-05-14T16:05:58.444012022+02:00" } ``` ### Sync response When `sync=true`, the response contains the full [classification result](/docs/classification-result/). :::note The `id` field in the response is the `classify_id` used in all other endpoints. ::: ## Get a classification result ' }]} /> Returns the full [response structure](/docs/response-structure/) for a given classification. ## Download annotated video ' }]} /> Returns the annotated highlight video for a classification. :::note Even if the video does not have the `.mp4` extension, it will be in MP4 format encoded with x264. ::: ## Delete a result ' }]} /> Deletes both the video and the classification results. --- # Getting Started URL: https://gaard.ai/docs/getting-started > Send your first video to Gaard for classification in three steps. This tutorial walks you through sending your first video to the Gaard Vision API and retrieving the classification result. By the end, you will have a working API call you can adapt for your integration. ### Prerequisites - A Gaard account with API access - `curl` installed on your machine - A video file to classify (MP4 or MOV) 1. ## Get your API token Create an API token in the Gaard app at [Settings > Platform > Integrations](https://app.gaard.ai/settings/platform/integrations). Once you have the token, export it as an environment variable so the commands in this tutorial can reference it: ```bash frame="terminal" title="terminal" export API_KEY="your-api-token" ``` 2. ## Get a sample video If you do not have a video file ready, download our sample clip or grab one from the [VIRAT dataset](https://viratdata.org/). Save the video file to your working directory. The examples below assume the file is named `video.mp4`. 3. ## Send a video for classification The classification endpoint accepts a `multipart/form-data` POST request. You upload the video file as a form field using `-F`, not as a JSON payload. ### Async mode (default) By default, classification runs asynchronously. The API accepts the video and returns an ID you use to poll for the result. :::tip[Optional] Clone the [gaard-api-docs](https://github.com/gaard-ai/gaard-api-docs) repository for convenience shell scripts shown in the **bash** tab. ::: ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/classify \ -H "Authorization: Bearer $API_KEY" \ -F "video=@video.mp4" ``` ```bash frame="terminal" title="terminal" bin/post-video video.mp4 "" false ``` The response contains the classification ID and a timestamp: ```json { "id": "66436fc66d24ab9cf81140eb", "accepted_at": "2024-05-14T16:05:58.444Z" } ``` Use the `id` value to retrieve the result: {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl https://vision.gaard.ai/api/result/66436fc66d24ab9cf81140eb \ -H "Authorization: Bearer $API_KEY" ``` {/* prettier-ignore */} ```bash frame="terminal" title="terminal" bin/get-result 66436fc66d24ab9cf81140eb ``` If the classification is still processing, poll this endpoint again after a few seconds until the result is available. ### Sync mode If you prefer to wait for the result in a single request, add `?sync=true`. The API blocks until classification is complete and returns the full result directly. {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl -X POST "https://vision.gaard.ai/api/classify?sync=true" \ -H "Authorization: Bearer $API_KEY" \ -F "video=@video.mp4" ``` {/* prettier-ignore */} ```bash frame="terminal" title="terminal" bin/post-video video.mp4 ``` ### Adding metadata (optional) You can attach a JSON metadata file alongside the video to provide additional context such as site and camera identifiers. Create a file named `metadata.json`: ```json { "site_id": "134188", "camera_id": "VI01" } ``` Then include it in the request as a second form field: {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/classify \ -H "Authorization: Bearer $API_KEY" \ -F "video=@video.mp4" \ -F "metadata=@metadata.json" ``` {/* prettier-ignore */} ```bash frame="terminal" title="terminal" bin/post-video video.mp4 metadata.json ``` ## What's next Now that you have successfully classified a video, explore the rest of the API: - [Endpoints](/docs/endpoints/): full endpoint reference for classify, result, annotate, and delete - [Response structure](/docs/response-structure/): understand the fields in a classification response - [Classification result](/docs/classification-result/): result types, statuses, and detailed examples - [Metadata](/docs/metadata/): the full metadata format and available fields - [Webhooks](/docs/webhooks/): receive async results via webhook instead of polling --- # API Tokens URL: https://gaard.ai/docs/guides/admin/api-tokens > Create credentials for programmatic access and scope them to a flow. Every programmatic call to Gaard authenticates with a bearer token. Gaard offers two kinds of credentials for different jobs: **API keys** scoped to a flow, and **personal access tokens** tied to your user. This guide shows how to create, scope, and revoke each one. ## Prerequisites - An **Administrator** account to manage API keys in **Settings → Platform → Integrations**. - The `gaard` CLI installed if you want to create personal access tokens. ## Which credential should I use? | | API key | Personal access token (PAT) | | --- | ------- | --------------------------- | | Created in | **Settings → Platform → Integrations** | The `gaard` CLI | | Scope | A single **flow** | Your **user** (all tenants you belong to) | | Best for | A service or integration that submits video to one flow | The CLI and personal automation | | Prefix | A generated key value | `pat_` | | Managed by | Any administrator | The token owner | Both are sent the same way: as a bearer token in the `Authorization` header: ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/classify \ -H "Authorization: Bearer $GAARD_TOKEN" \ -F "video=@video.mp4" ``` ## Create an API key An API key belongs to a [flow](/docs/guides/admin/flows/) and is the credential most integrations use to submit video. 1. Open **Settings → Platform → Integrations**. 2. Select the **API Key** integration. 3. Choose the **Flow** the key should authenticate against. 4. Give the key a **Name** that identifies where it is used, for example `edge-uploader`. 5. Copy the generated **API Key** value and store it securely: treat it like a password. 6. Select **Save**. The key appears in the integrations list with its flow. The key authenticates classify API calls for its flow. See [Getting Started](/docs/getting-started/) for a full request walkthrough. ## Create a personal access token Personal access tokens are user-scoped and created with the CLI. They carry a `pat_` prefix and can be given an expiry. ```bash frame="terminal" title="terminal" # Create a token gaard token create --name ci-bot # Create a token that expires in 30 days gaard token create --name nightly-job --expires 720h # List your tokens gaard token list ``` :::caution The raw token secret is shown **only once**, at creation time. Copy it immediately: Gaard stores only a hash and cannot show it again. ::: For the full command syntax, run `gaard token --help`. ## Rotate and revoke Rotate credentials on a schedule and whenever one may have been exposed. **API keys**: API keys do not rotate in place. To rotate one, create a new API key for the same flow, move your integration to the new key, then delete the old key from the integrations list using its row menu. **Personal access tokens**: set an expiry with `--expires` when you create a token, and revoke a token immediately when it is no longer needed: ```bash frame="terminal" title="terminal" gaard token list gaard token revoke ``` ## Next steps --- # Flows URL: https://gaard.ai/docs/guides/admin/flows > What a flow is, when to create one, and how tokens and webhooks scope to it. A flow is the unit that groups classification traffic and the credentials and integrations attached to it. This guide explains what a flow is, when one is enough, and how to scope tokens and webhooks to a flow. ## What a flow is A **flow** is a named pipeline for classification. Each flow ties together: - A **model**: the classification model the flow runs. - A **node**: the processing node that runs it. - The **integrations** attached to it: [API keys](/docs/guides/admin/api-tokens/), webhooks, and SFTP delivery. Every organization has a **default flow**, so you can start submitting video without creating one. When you create additional flows, each becomes an independent lane: an [API key](/docs/guides/admin/api-tokens/) belongs to exactly one flow, and a webhook or SFTP integration delivers results for the flow it is attached to. ## When to create a flow One flow is enough for most organizations. Create additional flows when you need to separate traffic that should be configured or delivered differently, for example: - **Separate sites or customers**: route each source through its own flow so results can be delivered to different endpoints. - **Separate environments**: keep test traffic on its own flow, away from production. - **Different delivery**: send one flow's results to a webhook and another's to an SFTP destination. If your traffic shares the same model and the same delivery, keep using the default flow. ## Create and manage flows 1. Open **Settings → Platform → Flows**. 2. Select **Create a flow**. 3. Enter a **Name**, then choose a **Model** and a **Node** from the options available to your organization. 4. Save the form. The flow appears in the list, showing its name, model, and node. To change or remove a flow, open its row menu and select **Edit** or **Delete**. ## Scope tokens and webhooks to a flow Integrations are attached to a flow when you create them: - **API keys**: when you add an **API Key** integration in **Settings → Platform → Integrations**, you select the flow it authenticates against. The key only submits video to that flow. See [API Tokens](/docs/guides/admin/api-tokens/). - **Webhooks and other delivery**: when you add a webhook or SFTP integration, you select the flow whose results it should deliver. See [Choosing a Delivery Method](/docs/guides/integrations/choosing-delivery/). In the integrations list, the **Flow** column shows which flow each integration is bound to, so you can confirm traffic and delivery line up. ## Next steps --- # Platform Configuration URL: https://gaard.ai/docs/guides/admin/platform-configuration > Tune thresholds, retention, and classification settings for your organization. Platform configuration shapes how Gaard scores video, how long it keeps data, and how annotated results look. This guide walks through each panel and what its settings control. ## Prerequisites - An **Administrator** account. Platform settings live under **Settings → Platform** and are only visible to administrators. Each panel is under **Settings → Platform → Settings**. Change a value, then select the panel's **Save** button to apply it: for example **Save Threshold Settings**. ## Threshold The **Threshold** panel controls the two boundaries that turn a raw `intrusion` score into a risk level. - **Enabled**: turn threshold tuning on or off. - **Range**: a dual slider from `0.00` to `1.00`. **Low** marks the boundary below which a result is considered `safe`; **High** marks the boundary above which a result is a clear `intrusion`. Scores between the two are `danger`. This panel is the configuration surface behind Gaard's risk model. For how scores become `safe`, `danger`, and `intrusion`, see [Risk, labels, and scores](/docs/risk-labels-scores/). ## Retention The **Retention** panel controls how long classified data is kept. - **Enabled**: when on, Gaard automatically deletes classified data older than the configured duration. - **Keep data for**: a duration string such as `720h`, `30d`, or `6m`. Days, months, and years are supported. When retention is disabled, data is kept for **at most 30 days**. For the data-protection context, see [Data Retention & GDPR](/docs/guides/deployment/retention-gdpr/). ## Classify The **Classify** panel tunes how the classifier scores video. - **Enabled**: turn classification on or off. - **Intrusion labels**: labels treated as intrusions during scoring. - **Excluded labels**: labels removed from the classifier output entirely. - **Classification sample rate**: classify 1 in N videos and return a mock result for the rest. Leave it empty to classify every video. :::caution Use the **Classification sample rate** only on test environments. On a production flow it means most videos are not actually classified. ::: ## Annotation The **Annotation** panel controls the appearance of annotated (highlight) videos and images. - **Enabled**: turn annotation output on or off. - **Draw grid overlay**: overlay a reference grid on annotated frames. - **Show all bounding boxes**: when off, only the labels that triggered the alert are drawn. - **Bounding box thickness (px)**: line thickness of detection boxes, from `1` to `5`. - **Frame border thickness (px)**: line thickness of the risk-colored frame border, from `1` to `5`. - **Rendered labels**: the set of labels drawn on annotated frames (for example `person`, `vehicle`, `intrusion`). ## Save a change Every panel follows the same pattern: 1. Open **Settings → Platform → Settings** and choose the panel. 2. Adjust the settings. 3. Select the panel's **Save** button. A confirmation appears when the update succeeds. ## Next steps --- # Users & Roles URL: https://gaard.ai/docs/guides/admin/users-roles > Invite members and understand what user, admin, and super roles can do. Manage who can access your organization and what they can do. This guide is for administrators: it covers the three roles, inviting members, and how members sign in and recover their accounts. ## Prerequisites - An **Administrator** or **Super administrator** account. The **Organization** and **Platform** sections only appear in **Settings** for administrators. ## Roles Gaard has three roles. Each role is a superset of the one below it. | Role | Label in the app | What it can do | | ---- | ---------------- | -------------- | | `user` | User | Use the Classify workspace: submit video, review classifications, search, and give feedback. | | `admin` | Administrator | Everything a user can do, plus manage members and edit **Platform** settings (thresholds, retention, classify, annotation, flows, integrations). | | `super` | Super administrator | Everything an administrator can do, plus manage organizations across tenants. | New members are created with the **User** role by default. Only the **Organization** and **Platform** settings are gated behind the administrator roles: the Classify workspace itself is available to every member. :::note The **Super administrator** role is reserved for Gaard platform operators who manage organizations across tenants. Its cross-organization tooling is outside the scope of these guides. When you assign roles as an administrator, you can grant **User** or **Administrator**. ::: ## Invite a member Adding a member creates their account and optionally emails them a sign-in link. 1. Open **Settings → Organization → Members**. 2. Select **Create user**. 3. Fill in the member's **Email**, **Firstname**, and **Lastname**. **Image URL** is optional. 4. Turn on **Send invitation email** to email the member a sign-in link. The link is valid for **7 days**. 5. Select **Save**. The member is created with the **User** role and, if you enabled the invitation, receives their sign-in link by email. ## Change a member's role 1. Open **Settings → Organization → Members**. 2. Find the member in the list and open the row menu, then select **Edit role**. 3. Choose **User** or **Administrator** and select **Save**. You can only assign a role at or below your own. Granting the **Super administrator** role requires an existing Super administrator. ## How members sign in From the sign-in screen, members have several options: - **Email and password**: the standard sign-in form. - **Magic link**: passwordless sign-in. The member enters their email and receives a one-time sign-in link that expires after **24 hours**. - **Google**: sign in with a Google account. Invited members who received an invitation email use the link in that email for their first sign-in, then set a password if they want to sign in with one later. ## Recover an account If a member forgets their password: 1. On the sign-in screen, select **Forgot password?** 2. Enter the account email to request a reset link. 3. Open the reset link from the email and set a new password on the **Set a new password** screen. As an alternative, a member can always sign in with a **magic link** without resetting their password. ## Next steps --- # Exclusion Zones URL: https://gaard.ai/docs/guides/classify/exclusion-zones > Mask regions of a camera view so they are ignored during classification. Exclusion zones let you silence motion you never care about: a busy road at the edge of frame, a flag, a swaying tree. This guide shows how to draw and manage zones for a camera in the web app. ## What exclusion zones do An exclusion zone is a masked region of a camera's field of view. Detections that fall inside an active exclusion zone are ignored during classification, so predictable, harmless motion in a known part of the frame stops generating alarms. Use exclusion zones when a specific area of a camera view is a consistent source of false positives: the classic cases are traffic along a road at the top of frame, vegetation moving in the wind, or a flag. Rather than repeatedly labeling those results as false positives, you mask the region once and the noise goes away. Zones are defined **per camera**: each camera has its own set of zones tied to its identity, and they only apply to that camera's classifications. :::note Exclusion zones shape *classification*, not recording. Masking a region means Gaard stops raising alarms from it: the underlying footage is unaffected. ::: ## Prerequisites - A camera that has recorded at least one video. The zone editor draws zones on top of a still from the camera's footage, so a camera with no footage yet cannot be edited: you will see a message that a zone backdrop appears once the camera has recorded footage. - Access to the **Sites** section of the web app. ## Open the zone editor 1. Go to **Sites** and open the site, then the camera you want to configure. 2. Select **Edit zone** on the camera. This opens the zone editor at `/sites//camera//zones`. The editor has two panes: the camera view on the left, where you draw and adjust zones, and the **Exclusion zones** panel on the right, which lists every zone on the camera. ## Draw a zone 1. In the **Exclusion zones** panel, select the **add** (**+**) button to start a new zone. 2. Draw the region on the camera view to cover the area you want to mask. Adjust its shape directly on the still until it fits the area. 3. The new zone appears in the panel as a numbered **Scenario**, enabled by default. Each zone is called a *Scenario* because a zone can carry activation rules, not just a shape: see [Scheduling a zone](#scheduling-a-zone) below. ## Manage zones Every zone in the panel has a toggle, a delete control, and an expandable detail section. ### Enable, disable, and status Use the toggle on each zone to enable or disable it. A zone reports one of three states: | State | Meaning | | --- | --- | | **enabled** | The zone is on and currently masking its region. | | **idle** | The zone is on but outside its scheduled window, so it is not masking right now. | | **disabled** | The zone is off and has no effect. | Disable a zone to turn it off temporarily without losing its shape or settings; delete it (the trash control) to remove it permanently. ### Scheduling a zone Expand a zone and open its **Settings** tab to restrict *when* it is active. This is useful when a region is only a nuisance at certain times: for example, masking a car park during working hours but not overnight. You can combine: - **Hour**: a start and end time of day. - **Date**: a start and end date range. - **Week days**: the specific days of the week the zone applies. When a zone is enabled but the current time falls outside its schedule, it shows as **idle** and does not mask its region until the next active window. :::caution A masked region is invisible to classification while the zone is active. Keep exclusion zones tight around the genuine nuisance so you do not accidentally hide a region where a real intrusion could occur. ::: ### Zone information The **Information** tab on each zone records who created it, when, and who last modified it: useful for auditing changes when several operators share a camera. ## Next steps --- # Exporting Data URL: https://gaard.ai/docs/guides/classify/exporting > Export results as CSV, build datasets, and download annotated video. Everything Gaard produces can leave the platform. This guide covers the three export paths: CSV reports for analysis, datasets for machine learning, and annotated video for evidence. ## CSV export A CSV report is a spreadsheet-friendly snapshot of results, suited to reporting, auditing, and offline analysis. You can export from two places, depending on what you want: - **From the review workspace**: exports the classifications matching your current filters and time range. Set up the view you want first (see [Search & Filtering](/docs/guides/classify/search-filtering/)), then export. - **From the Labels view**: exports the labels your team has recorded (see [Labeling & Feedback](/docs/guides/classify/labeling-feedback/)). 1. Open the actions menu in the [review workspace](/docs/guides/classify/reviewing/) or the **Labels** view. 2. Select **Generate a CSV report**. 3. Confirm **Generate** in the dialog. The report is built server-side and the download starts automatically when it is ready. :::tip Narrow your filters and time range before exporting from the review workspace. The CSV reflects exactly the results currently in view, so a tighter filter means a smaller, more relevant report. ::: ## Dataset export A dataset packages labeled clips and their metadata into a single archive (tar format), ready to feed into a machine-learning workflow. Unlike a CSV (which is just tabular data) a dataset bundles the underlying material. Dataset generation runs as a background job because it can take time to assemble. 1. In the **Labels** view, open the actions menu and select **Generate a dataset**. 2. Confirm **Generate**. A new job appears in the list and reports its **progress** as it runs. 3. When the job is **Done**, select its filename to download the archive. Dataset jobs persist in the list, so you can start a large export, leave the page, and come back to download it once it has finished. ## Downloading annotated video The annotated video is the highlight clip for a classification with the model's detections drawn on it as bounding boxes: the same footage you see in the review player. It is the most useful artifact to hand to a colleague or attach to an incident, because it shows *what* Gaard detected and *where*. Retrieve it from the API with the classification's `classify_id`: {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl https://vision.gaard.ai/api/video/annotate/66436fc66d24ab9cf81140eb \ -H "Authorization: Bearer $API_KEY" \ -o annotated.mp4 ``` :::note The response is always MP4 (x264), even if the returned file does not carry a `.mp4` extension. ::: For the full endpoint reference, see [Endpoints](/docs/endpoints/). ## Next steps --- # Labeling & Feedback URL: https://gaard.ai/docs/guides/classify/labeling-feedback > Mark true and false positives, apply custom labels, and improve your model over time. Labels are how operators tell Gaard whether a classification was right. This guide shows how to attach feedback labels from the web app and the API, and how those labels feed back into model quality. ## Why feedback matters Every classification carries a model-derived risk level, but only a human knows the ground truth. A feedback label records that ground truth against a specific result: was this alarm real, or was it a branch moving in the wind? Labeled results give you three things: - **Measured accuracy.** Confirmed true and false positives let you quantify how the model performs on *your* cameras and scenes, not a generic benchmark. - **Tuning signal.** Systematic false positives on a camera are the input to threshold changes and [exclusion zones](/docs/guides/classify/exclusion-zones/): you cannot fix what you have not measured. - **Training data.** Labeled clips are the highest-value examples for improving the model over time. Feedback closes the loop between operators and the classifier. Labeling is quick and additive: label the results you are confident about, and skip the rest. Even a small, consistent labeling habit sharpens the accuracy picture for your tenant. ## Feedback labels The four standard feedback labels describe the relationship between what Gaard flagged and what actually happened: | Label | Meaning | Use when | | --- | --- | --- | | `TP` | True positive | Gaard correctly identified a real threat. | | `FP` | False positive | Gaard flagged the video but there was no real threat. | | `FN` | False negative | Gaard missed a real threat. | | `TN` | True negative | Gaard correctly classified the video as safe. | If you do not need the full confusion-matrix vocabulary, the simpler `true` and `false` labels are also accepted. Alongside feedback labels, Gaard ships a set of **descriptive** standard labels (categories such as `person`, `vehicle`, `animal`, and `plant`, plus environmental factors like `rain` and `wind`) that let you record *what was in the clip* rather than only whether the alarm was correct. See the [Labels reference](/docs/labels/) for the complete list returned by the API. ## Custom labels Standard labels rarely cover every operational distinction a team cares about: a specific gate, a known vehicle, a recurring nuisance. Custom labels are free-text labels you define for your own tracking. - Create a custom label inline the first time you need it: type a new value into the label selector and add it. - Custom labels are scoped to your tenant. Once created, a custom label is remembered and offered as a suggestion the next time you label a result. - Any string value is accepted and stored. Custom labels also appear in the list returned by the API, so they are available to integrations as well as the web app. :::tip Agree on a small, shared vocabulary of custom labels before your team starts using them. A handful of consistent labels is far more useful for analysis than dozens of near-duplicates. ::: ## Label from the web app You label results while reviewing them, so feedback is a natural part of the review workflow rather than a separate chore. 1. **Open a classification** in the [review workspace](/docs/guides/classify/reviewing/) and select the clip you want to label. 2. **Open the Score tab.** The label selector and comment field sit alongside the per-clip scores, so you can see the model's confidence while you decide. 3. **Select one or more labels.** Choose from the standard labels or start typing to create and apply a custom label. Selections save as you make them. 4. **Add a comment (optional).** Use the comment field to explain the label (for example, *"False alarm) wind moving a branch"*. Comments are stored with the label and are visible to teammates reviewing the same result. To browse and manage everything your team has labeled, open the **Labels** view. It lists labeled results with filtering, and it is also where you export labels as CSV or build a dataset: see [Exporting Data](/docs/guides/classify/exporting/). ## Label from the API Integrations can label results programmatically, which is useful when an upstream alarm platform or operator tool already knows the outcome. Attach one or more labels and an optional comment to an existing classification with its `classify_id`: {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/label/66436fc66d24ab9cf81140eb \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"labels": ["FP"], "comment": "False alarm - wind moving a branch"}' ``` To discover which labels are available for your tenant (standard and custom) list them with `OPTIONS /api/label`. For the full request and response schema, including field types and the recommended label values, see the [Labels reference](/docs/labels/). ## Next steps --- # Reviewing Classifications URL: https://gaard.ai/docs/guides/classify/reviewing > Work through classification results in the review workspace: results grid, clip player, per-clip risk, and detail tabs. The review workspace is where operators spend most of their time: triaging alarms, watching the clip behind each one, and escalating what matters. This guide shows how to move through results efficiently. ## The review workspace The workspace has three parts, all scoped to the [flow](/docs/guides/admin/flows/) selected in the top bar and to the time range you choose: - **A histogram** across the top, showing alarm counts over time, split by risk level. - **A results grid** below it, one row per classification. - **An action bar** with the [filter builder](/docs/guides/classify/search-filtering/), a flow switcher, the date-range selector, and export actions. Switch flows or change the date range at any time: the histogram and grid refetch together. ## The results grid Each row summarises one classification. The columns are: | Column | What it shows | | --- | --- | | **Alarm** | A thumbnail, the classification ID, the video name, and the resolved **Site** and **Camera** (both are links that filter the grid). A marker appears when an [exclusion zone](/docs/guides/classify/exclusion-zones/) was applied. | | **Risk** | The [risk badge](/docs/risk-labels-scores/) plus any labels attached to the result. | | **Video** | Source resolution, frame rate, frame count, and duration. | | **Classify Time** | How long the model took to classify the clip. | | **Date** | When the classification was recorded. | Select any row to open its clip. The **⋯** menu on a row jumps straight to the classification **Details** or the camera's **Zones** editor. To narrow the grid, use the [filter builder](/docs/guides/classify/search-filtering/) in the action bar. ## Watching a clip Selecting a row opens the classification in a modal. The clip plays on the left with any exclusion zones drawn over it; the right side carries the detail tabs. 1. Select a row to open the clip player. 2. Watch the clip. Use **‹** and **›** to step to the previous or next result in the grid without closing the player: so you can work through a filtered list in order. 3. Take action from the footer: **Edit zones** to open the camera's zone editor, **Reclassify** to re-run the clip, or **Close** to return to the grid. ## Understanding per-clip risk The player header shows the clip's risk as a badge: **safe**, **danger**, or **intrusion**. This is the same three-level scale used throughout Gaard, derived from the model's `intrusion` score against your configured thresholds. Read [Risk, labels, and scores](/docs/risk-labels-scores/) for the full derivation. In short: - **safe**: normal background activity; may be filtered out. - **danger**: ambiguous activity; **must be reviewed by an operator**. - **intrusion**: high-confidence intrusion; **must be reviewed and escalated**. Anything that is not `safe` stays visible so an operator can validate it. ## Result, score, and media detail tabs Alongside the clip, tabs give progressively deeper detail: | Tab | Contents | Availability | | --- | --- | --- | | **Scores** | The per-concept confidence scores behind the risk verdict (for example `intrusion`, `person`, `vehicule`). | All operators | | **Mediainfo** | Technical properties of the source video: codec, resolution, frame rate, and other track details. | All operators | | **Result** | The raw classification result payload. | Admins | | **Task** | The full internal task record. | Super admins | The **Scores** tab is the fastest way to understand *why* a clip was scored the way it was: a high `intrusion` score with a supporting `person` or `vehicule` score tells a very different story from a lone `flag` or `wind` score. ## Sharing a view To point a teammate at exactly what you are looking at, copy the page address and send it. Gaard captures the current view into a short link so they open the same view: as long as they belong to the same organization. The browser **Back** and **Forward** buttons also step through the views you have visited, like undo/redo for your workspace. ## Next steps --- # Search & Filtering URL: https://gaard.ai/docs/guides/classify/search-filtering > Build filters over sites, cameras, risk, labels, and video properties, and read the histogram to narrow results. Gaard indexes every classification so you can slice results by site, camera, risk, label, and video properties. This guide covers the filter builder and the histogram in the [review workspace](/docs/guides/classify/reviewing/). ## The filter builder The filter bar sits in the action bar of the review workspace, next to the flow switcher and date selector. It builds a query as a row of editable tokens: each token is a **facet**, an **operator**, and one or more **values**. 1. Select the bar (placeholder **Filter alarms…**) to open the facet list. 2. Pick a facet: or start typing its name to narrow the list. You can also type `facet:value` directly, for example `risk:intrusion`. 3. Choose an operator and value. Multi-value facets like **Risk** and **Label** let you select several values at once. 4. Add more tokens the same way. Tokens combine to narrow results. Remove a token with its **✕**, or clear everything with the **✕** at the end of the bar. Type-ahead is keyboard-first: arrow keys move through the list, **Enter** or **Tab** confirms the highlighted facet, and **Backspace** on an empty bar re-opens the last token to edit it. Text that does not match a facet becomes a free-text search. ### Available facets | Facet | Filters on | Operators | | --- | --- | --- | | **Site** | The site a clip came from | is / is not | | **Camera** | A camera within the selected site | is / is not | | **Risk** | `safe`, `danger`, or `intrusion` | is / is not / in / not in | | **Label** | Labels attached to results | is / is not / in / not in | | **Video** | Text match on the video name | contains | | **Classify Time** | How long classification took | ≥ / ≤ / = / ≠ | | **Video duration** | Length of the source clip | ≥ / ≤ / = / ≠ | | **Resolution** | Source resolution (e.g. `1920x1080`) | is | | **FPS** | Source frame rate | ≥ / ≤ / = / ≠ | | **Frames** | Source frame count | ≥ / ≤ / = / ≠ | | **Has zones** | Whether an exclusion zone was applied | is | | **Has error** | Whether classification errored | is | :::note **Camera** becomes available only once a **Site** is set, and its options are scoped to that site: camera identifiers repeat across sites, so choosing the site first is what makes the camera list meaningful. Removing the Site also clears the Camera. ::: ## Reading the histogram Above the grid, the histogram shows alarm counts over time, bucketed by risk level. It reflects the same filters as the grid, so as you add tokens the distribution updates with them. The histogram always spans the **full selected time range**. Brushing a range of bars zooms the **grid** into that window without changing the chart: a quick way to drill into a spike and then step back out. Use the date selector in the action bar to change the overall range. ## Reusing a filter The **time range** you select is reflected in the page address, so you can bookmark or share a filtered time window. Drill-down links elsewhere in the app (for example a site or camera on the dashboard) open the workspace with the matching filter already applied. :::note The filter tokens themselves are scoped to your current session in the workspace and reset when you leave it. Rebuild a frequent filter from the facet list, or reach it through a drill-down link. ::: ## Next steps --- # Submitting Video URL: https://gaard.ai/docs/guides/classify/submitting-video > Send video for classification from the web app or the API, with metadata, in sync or async mode. This guide covers every way to get a video into Gaard Classify: the manual uploader in the web app, the classify API for automated pipelines, and re-running a video that is already stored. ## Choose how to submit | Method | Best for | Metadata support | | --- | --- | --- | | **Web app uploader** | Ad-hoc checks, spot testing, a handful of clips | No: use the API to attach metadata | | **Classify API** | Camera integrations, batch pipelines, anything automated | Yes: `metadata.json` | | **Re-classify** | Re-running a stored video after a configuration change | Inherits the original submission | ## Upload from the web app The uploader is a three-step flow: select files, upload, done. It accepts several files at once and shows per-file progress. 1. Open the **Upload** view in the web app. 2. Drag video files onto the drop zone, or select **Open File Dialog** and pick them from your machine. Each selected file appears in a list with its name and size. 3. Select **Upload and classify**. Progress is shown as a percentage next to each file. 4. When every file reaches 100%, you see **All video files have been sent for classification!**. Select **Upload again** to submit more, or open the [review workspace](/docs/guides/classify/reviewing/) to work through the results. :::note The web uploader does not attach metadata. To tag a submission with a site or camera identifier, use the [API](#submit-via-the-api) with a `metadata.json` file. ::: ## Submit via the API Automated integrations send video to `POST /api/classify` as a `multipart/form-data` request. The video is uploaded as a form field named `video`: not as a JSON body. {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/classify \ -H "Authorization: Bearer $API_KEY" \ -F "video=@video.mp4" ``` {/* prettier-ignore */} ```bash frame="terminal" title="terminal" bin/post-video video.mp4 "" false ``` The full endpoint reference (every parameter, response shape, and related endpoint) lives in [Endpoints](/docs/endpoints/). You need an API token to authenticate; see [API Tokens](/docs/guides/admin/api-tokens/). ## Synchronous vs asynchronous The `sync` query parameter decides whether the request waits for the result. | Mode | Request | Response | Use it when | | --- | --- | --- | --- | | **Asynchronous** (default) | `POST /api/classify` | An `id` and an `accepted_at` timestamp: poll for the result later, or receive it by [webhook](/docs/guides/integrations/choosing-delivery/) | Throughput matters and you process results out of band | | **Synchronous** | `POST /api/classify?sync=true` | The full [classification result](/docs/classification-result/) in the response body | You need the verdict inline in a single call | The asynchronous response returns immediately: ```json { "id": "66436fc66d24ab9cf81140eb", "accepted_at": "2024-05-14T16:05:58.444Z" } ``` The `id` is the `classify_id` used by every other endpoint: retrieve the result with `GET /api/result/`. :::tip Prefer asynchronous submission for camera pipelines. Waiting for a synchronous result ties up the connection for the full classification time, which is wasteful at scale. Choose a [delivery method](/docs/guides/integrations/choosing-delivery/) (polling, webhooks, or SFTP) to collect results. ::: ## Attaching metadata A `metadata.json` file lets you tag a submission with the site and camera it came from, so results are attributable in the [review workspace](/docs/guides/classify/reviewing/) and can be filtered by site and camera. All fields are optional. ```json { "site_id": "134188", "camera_id": "VI01" } ``` Send it as a second form field named `metadata`: {/* prettier-ignore */} ```bash frame="terminal" title="terminal" curl -X POST https://vision.gaard.ai/api/classify \ -H "Authorization: Bearer $API_KEY" \ -F "video=@video.mp4" \ -F "metadata=@metadata.json" ``` {/* prettier-ignore */} ```bash frame="terminal" title="terminal" bin/post-video video.mp4 metadata.json ``` The full field list and fallback behaviour are documented in [Metadata](/docs/metadata/). ## Re-classifying a stored video When you change a threshold or add an [exclusion zone](/docs/guides/classify/exclusion-zones/), you often want to see how an existing result would change: without re-uploading the file. Re-classify runs the stored video through classification again. 1. In the [review workspace](/docs/guides/classify/reviewing/), open a classification to bring up its detail view. 2. Select **Reclassify**. The button changes to **Sent** while the video is re-processed, and the view refreshes to the new result. Re-classifying reuses the original video and metadata, so the new result is directly comparable to the old one. ## Next steps --- # Core Concepts URL: https://gaard.ai/docs/guides/concepts > Tenants, solutions, flows, sites and cameras, risk levels, labels, and the classification lifecycle. Every Gaard surface (web app, API, CLI) shares the same vocabulary. This page explains the concepts you will meet everywhere else in the documentation. Read it once and the rest of the guides will read faster. ## Tenants and isolation A **tenant** is the isolation boundary in Gaard. It owns your cameras, classifications, labels, configuration, and users, and nothing crosses from one tenant to another. When you sign in to the web app or authenticate an API token, you are always operating within a single tenant. A tenant name is also a database identifier, so it follows strict rules: - starts with a lowercase letter - uses only `a-z`, `0-9`, `-`, and `_` - is at most 64 characters - is never `admin`, `config`, or `local` Because the tenant is the isolation boundary, every classification result carries its `tenant`, and every API token is scoped to one tenant. Shared views (for example, shareable links) only resolve for people in the same organization. ## Solutions and entitlements A **solution** is an entitleable product line. Your account is entitled to one or both: - **Classify**: AI video classification (the subject of these guides). - **Replay**: cloud video recording and playback. Entitlements drive what you see: the solutions your account holds determine which apps appear in the web app switcher. Entitlements are about discoverability and access: they decide which surfaces are available to you, not how any single clip is scored. ## Flows A **flow** is a scoping and configuration unit within a tenant. Where the tenant is the hard isolation boundary, a flow lets you partition work inside it: for example, one flow per customer, region, or integration. Flows matter when you: - **Scope a token or webhook.** An API token or webhook can be tied to a specific flow so its traffic and delivery stay separated. - **Review a subset of alarms.** The web app lets you switch the active flow to focus the dashboard and review workspace on one slice of activity. See [Flows](/docs/guides/admin/flows/) for when to create one and how to scope tokens and webhooks to it. ## Sites and cameras Gaard organizes cameras under sites: - A **site** is a physical location. Its identifier is the **SID**. - A **camera** belongs to a site. Its identifier is the **CID**. Both the Classify and Replay pipelines key on CID/SID, so a classification result and a recorded segment refer to the same physical camera, and the web app routes and filters on them. There are two kinds of camera: | Camera type | Created by | Carries | | --- | --- | --- | | **API camera** | Classify traffic, automatically | Only an identity derived from request metadata: no stream or credentials. | | **Managed camera** | Configured explicitly | A full stream and credential configuration (used by Replay). | ### How metadata maps to a camera When you submit a clip through the API, its metadata carries loose external identifiers. Gaard normalizes them into a stable identity: - the **site key** is `site_id` - the **camera key** is `camera_id` The first time Gaard sees a new site/camera key, it creates an **API camera** (and its site) automatically; subsequent clips with the same keys resolve to the same camera. Clips with no usable site or camera key are classified but not attached to a camera. See [Metadata](/docs/metadata/) for the full field list. ## Risk levels Gaard exposes **three and only three** risk levels: - `safe` - `danger` - `intrusion` The risk level is **derived exclusively from the `intrusion` score** using two configurable thresholds. The other scores are exposed for observability, but they do not drive the risk level. | Intrusion score | Risk level | Operational meaning | | --- | --- | --- | | `< low_threshold` | `safe` | Normal background activity. May be filtered out. | | `>= low_threshold` and `< high_threshold` | `danger` | Ambiguous or suspicious. Must be reviewed. | | `>= high_threshold` | `intrusion` | High-confidence intrusion. Must be reviewed and escalated. | Typical thresholds are `low_threshold = 0.2` and `high_threshold = 0.8`, and they are configurable per tenant. Anything that is not `safe` stays visible to operators: it is never discarded automatically. For the full derivation, see [Risk, labels, and scores](/docs/risk-labels-scores/). ## Labels Where a **score** is the model's raw confidence and the **risk level** is the derived decision, a **label** is a tag attached to a classification. There are three kinds: - **Standard labels**: the built-in concepts Gaard recognizes, such as `person`, `vehicle`, `animal`, `flag`, `rain`, and `wind`. - **Custom labels**: any tag your team defines for its own tracking. - **Feedback labels**: an operator's verdict on whether the model was right: | Label | Meaning | | --- | --- | | `TP` | True positive: Gaard correctly flagged a threat. | | `FP` | False positive: Gaard flagged a clip with no real threat. | | `FN` | False negative: Gaard missed a real threat. | | `TN` | True negative: Gaard correctly classified a clip as safe. | Feedback labels are how the model improves over time. See the [Labels API](/docs/labels/) for the endpoints, and [Labeling & Feedback](/docs/guides/classify/labeling-feedback/) for the operator workflow. ## The classification lifecycle Every clip you submit becomes a **classification**, identified by a `classify_id` (the `id` in every API response). You use that ID to poll for the result, attach labels, or download the annotated video. A classification moves through a set of statuses: | Status | Meaning | | --- | --- | | `accepted` | The clip has been accepted for analysis. | | `in-progress` | Analysis is running. | | `done` | Analysis is complete; the result is available. | | `error` | An error occurred during analysis. | | `timeout` | Analysis timed out. | ```mermaid graph LR A("accepted") --> B("in-progress") B --> C("done") B --> D("error") B --> E("timeout") ``` Submit a clip asynchronously and you get back a `classify_id` immediately, then poll until the status is `done`; submit synchronously (`?sync=true`) and Gaard blocks until the result is ready. Either way, the finished result (risk level, labels, scores, and annotated highlight) lives against that `classify_id` and is reachable from every surface. See [Classification result](/docs/classification-result/) for the full result structure. ## Next steps - [Web App Quickstart](/docs/guides/quickstart-webapp/): see these concepts in the review workspace. - [API Quickstart](/docs/getting-started/): submit your first clip and watch it move through the lifecycle. - [Risk, labels, and scores](/docs/risk-labels-scores/): the detailed threshold reference. --- # Cloud vs On-Premises URL: https://gaard.ai/docs/guides/deployment/cloud-vs-on-premises > The two ways to run Gaard (managed cloud and self-hosted on-premises) and which one fits your constraints. Gaard ships as two distributions of the same platform: a **managed cloud service** that Gaard operates for you, and a **self-hosted on-premises bundle** that you run on your own hardware. Both distributions run the same core components and expose the same web app, API, and CLI. This page explains what each includes and how to choose between them. ## Cloud The cloud distribution is a fully managed service. You sign in at [app.gaard.ai](https://app.gaard.ai), submit video through the API at `vision.gaard.ai`, and Gaard runs everything behind those endpoints. Key properties: - **Nothing to install or operate.** There is no infrastructure for your team to provision, patch, or monitor. You manage users, tokens, and platform settings from the web app; Gaard runs the underlying services. - **Continuous, zero-downtime updates.** New model versions, features, and fixes are rolled out continuously without a maintenance window or an upgrade step on your side. - **EU data residency.** Your videos, classification results, and account data are stored and processed in the European Union. - **Scales on demand.** Ingest and classification capacity is managed for you, so bursts of submissions do not require you to size hardware in advance. The cloud distribution is the default and the fastest way to start: you can create a tenant and classify your first video without any deployment work. See [Quickstart: Web App](/docs/guides/quickstart-webapp/) to begin. ## On-premises The on-premises distribution is a self-hosted bundle that you deploy on your own servers with Docker Compose. It runs the same platform as cloud, but inside your network and under your operational control. The bundle includes every component the platform needs to run: - the Gaard **API** - the **classification worker** (available in CPU and GPU builds, so you can match the hardware you have) - **MongoDB** for operational data - **ClickHouse** for analytics - **Redis** for coordination - **MinIO** for object storage (videos and artifacts) Key properties: - **Runs in your environment.** All video and data stay on infrastructure you own and control. Nothing leaves your network unless you configure an outbound integration. - **Air-gapped installs are supported.** Container images can be exported to archive files on a connected machine and loaded onto a disconnected host, so the platform can run with no internet access. - **You control upgrades.** You choose when to apply a new release. Each release ships with step-by-step upgrade instructions, including any database migrations, provided by your Gaard contact. - **Runs on your hardware.** GPU acceleration is optional: a CPU-only build is available where a GPU is not present. :::note On-premises deployment requires Docker and, for GPU acceleration, the NVIDIA driver and container toolkit. Your Gaard contact provides the install bundle and image access. ::: ## Choosing a distribution Both distributions deliver the same product. The choice comes down to where your data must live and who operates the platform. | Consideration | Cloud | On-premises | | --- | --- | --- | | Who operates it | Gaard | You | | Where data lives | Gaard cloud (EU) | Your infrastructure | | Setup effort | None: sign in and go | Deploy the Docker Compose bundle | | Updates | Continuous, zero-downtime | You apply releases on your schedule | | Air-gapped / no internet | Not applicable | Supported | | Hardware | Managed for you | Your servers (CPU or GPU) | | Scaling | Managed on demand | Sized by you | Choose **cloud** if you want the fastest path to production, automatic updates, and no infrastructure to run, and EU data residency meets your requirements. Choose **on-premises** if a policy or regulatory constraint requires video and data to remain inside your own network, if you operate in an air-gapped environment, or if you need full control over when upgrades are applied. :::tip The security model (tenant isolation, the token model, and encryption at rest) is identical across both distributions. See [Security & Data Isolation](/docs/guides/deployment/security/) for the details evaluators ask about. ::: ## Next steps - [Security & Data Isolation](/docs/guides/deployment/security/): how Gaard isolates tenants, scopes tokens, and protects data at rest. - [Data Retention & GDPR](/docs/guides/deployment/retention-gdpr/): how long data is kept, and how erasure works. - [Core Concepts](/docs/guides/concepts/): tenants, solutions, flows, and the classification lifecycle. --- # Data Retention & GDPR URL: https://gaard.ai/docs/guides/deployment/retention-gdpr > How long Gaard keeps your data, how to change it, deleting classifications, and honoring erasure requests. This page explains how long Gaard keeps classification data, how to change that, and the tools the platform provides to support your data-protection obligations, including erasure. It applies to both the [cloud and on-premises distributions](/docs/guides/deployment/cloud-vs-on-premises/). ## Retention settings Gaard applies a **retention window** to classification data: classified videos, their results, and the artifacts derived from them are automatically deleted once they are older than the window. By default, data is kept for **30 days** and then removed. Retention is configured per tenant in the web app at [Settings → Platform → Configuration → Retention](https://app.gaard.ai/settings/platform/configuration/retention). From there you can: - **change the retention window** to match your policy, and - **enable or disable** automatic deletion. Cleanup runs on a schedule in the background. When data ages past the window, the platform deletes the results, the video, and their dependent artifacts from both the database and object storage. Per-camera overrides are supported, so a specific camera can keep data for a different period than the tenant default. :::note Retention is about **automatic, time-based** deletion. To remove a specific item immediately, delete it directly (see below). ::: ## Deleting a classification To remove a single classification on demand (including both the stored video and its results) delete it by its `classify_id`: ```bash frame="terminal" title="terminal" curl -X DELETE https://vision.gaard.ai/api/classify/ \ -H "Authorization: Bearer " ``` This deletes both the video and the classification results for that item. See the [Delete a result](/docs/endpoints/#delete-a-result) endpoint reference for details. ## GDPR erasure Beyond routine retention and per-item deletion, Gaard provides an **audited subject-erasure** path for handling a data subject's erasure request (for example, a GDPR right-to-erasure request). Because footage is not indexed by personal identity, an authorized operator (such as your data protection officer) supplies the specific records to erase. The platform then: - deletes the targeted dataset artifacts (the clips and their associated files) from storage, - removes the associated labels so the data does not reappear, and - writes an **audit record** capturing who performed the erasure, the subject reference, the reason, and exactly what was removed. This path is **restricted to the highest-privilege role** and always requires a subject reference and at least one target, so erasure is deliberate and traceable. If you need to run an erasure, contact your Gaard administrator or support. :::caution Erasure permanently deletes the targeted data. It is the sanctioned way to remove a subject's data on request, and the action is recorded for audit. ::: ## EU data residency On the **cloud** distribution, your videos, classification results, and account data are stored and processed in the European Union. On the **on-premises** distribution, all data stays on the infrastructure you operate: nothing leaves your network unless you configure an outbound integration. See [Cloud vs On-Premises](/docs/guides/deployment/cloud-vs-on-premises/) for the full comparison. ## Next steps - [Security & Data Isolation](/docs/guides/deployment/security/): tenant isolation, tokens, and encryption at rest. - [Cloud vs On-Premises](/docs/guides/deployment/cloud-vs-on-premises/): where your data lives in each distribution. - [Exporting Data](/docs/guides/classify/exporting/): get your data out before it ages past retention. --- # Security & Data Isolation URL: https://gaard.ai/docs/guides/deployment/security > How Gaard isolates tenants, scopes access tokens, and encrypts sensitive data at rest. This page explains the security model evaluators ask about: how organizations are isolated from one another, how the different access tokens work, and how sensitive data is protected at rest. It applies to both the [cloud and on-premises distributions](/docs/guides/deployment/cloud-vs-on-premises/): the model is identical. :::note Gaard does not currently hold a third-party security certification such as SOC 2. This page describes the technical controls that are built into the platform, not a certification status. ::: ## Tenant isolation A **tenant** is the top-level boundary for an organization's data. Every video, classification result, user membership, and configuration value belongs to exactly one tenant, and Gaard partitions data by tenant at the database level: each tenant's operational data lives in its own dedicated database, keyed by the tenant name. This has two consequences that matter for an evaluator: - **No cross-tenant reads.** A request is bound to a single tenant for its entire lifetime. Queries only ever run against that tenant's database, so one organization cannot see, list, or address another organization's data. - **Access is scoped to the tenant, not shared globally.** API tokens are issued for a specific tenant (see below), and a user only sees the tenants they have been granted membership in. See [Core Concepts](/docs/guides/concepts/) for how tenants relate to solutions, flows, sites, and cameras. ## The token model Gaard uses three distinct kinds of credential, each for a different caller. ### Browser sessions When a person signs in to the web app at [app.gaard.ai](https://app.gaard.ai), the platform issues an **encrypted session cookie**. Sign-in supports email and password, one-time **magic links**, and Google sign-in. The session carries the user's identity and their currently selected tenant; switching tenants changes what the session can address, always within the tenants the user is a member of. ### API tokens (tenant-scoped) Machine-to-machine access (the classification API, webhooks, and integrations) uses **API tokens**. You create them in the web app at [Settings → Platform → Integrations](https://app.gaard.ai/settings/platform/integrations), and send them on every request in the `Authorization` header: ```bash frame="terminal" title="terminal" curl https://vision.gaard.ai/api/result/ \ -H "Authorization: Bearer " ``` Each API token is **scoped to a single tenant** and, optionally, to a specific [flow](/docs/guides/admin/flows/). A request presenting the token can only ever act within that tenant. For how to create and manage these tokens, see [API Tokens](/docs/guides/admin/api-tokens/). ### Personal access tokens (CLI) The Gaard CLI authenticates with a **personal access token (PAT)**. A PAT is tied to a user account and is recognizable by its `pat_` prefix. PATs are created either through a browser-approved login flow or by signing in with your credentials from the CLI. A PAT is shown in full **only once**, at creation time. Gaard stores only a SHA-256 hash of the token, never the token itself, so a token value cannot be recovered from the database: it can only be revoked and replaced. You can list and revoke your own tokens at any time. ## Encryption at rest Gaard encrypts the sensitive values it must store so that they are not readable in the database: - **Integration credentials and secrets** (such as the credentials used to deliver results to an external system) are encrypted with **AES-GCM** before they are written to the database, and decrypted only when the platform needs to use them. In the API and web app, these values are **redacted** (shown as `********`) rather than returned in plaintext. - **User passwords** are never stored in plaintext or reversible form; they are hashed with **bcrypt**. - **Personal access tokens** are stored as **SHA-256 hashes**, as described above. - **Session cookies** are encrypted. :::caution For on-premises deployments, the encryption key and session secret are configuration values you set and control. They must be set before the platform starts, and you are responsible for keeping them safe: losing the encryption key makes encrypted values unrecoverable. ::: ## Next steps - [Data Retention & GDPR](/docs/guides/deployment/retention-gdpr/): how long data is kept, deleting classifications, and erasure. - [Cloud vs On-Premises](/docs/guides/deployment/cloud-vs-on-premises/): where your data lives in each distribution. - [API Tokens](/docs/guides/admin/api-tokens/): creating and scoping tokens. - [Users & Roles](/docs/guides/admin/users-roles/): inviting members and assigning roles. --- # Azursoft Integration URL: https://gaard.ai/docs/guides/integrations/alarm-platforms/azursoft > Connect Azursoft supervision to Gaard with a flow-scoped API key: Azursoft submits clips and shows the verdict to your operators. [Azursoft](https://www.azursoft.com) builds security-supervision software for monitoring stations, and was the first alarm platform to ship a native Gaard integration. Azursoft drives the connection itself: its platform submits alarm video to the Gaard classify API and reads the verdict back, so operators see the AI verification directly in their Azursoft console, with no receiver or webhook to host on your side. This page is part of [Alarm Platforms](/docs/guides/integrations/alarm-platforms/). ## How it works The integration is a flow-scoped **API key** that Gaard generates for Azursoft: - You create an **Azursoft** integration in Gaard, which issues an API key bound to one [flow](/docs/guides/admin/flows/). - You configure that key in Azursoft's Gaard connector. - Azursoft authenticates with the key as a bearer token and submits each alarm clip to the classify API; the classification lands in your Gaard workspace like any other, and the verdict returns to Azursoft. Because Azursoft drives the exchange, there is nothing to expose or keep reachable on the Azursoft side: the key is the whole contract. ## Prerequisites - An **admin** role in the tenant. - A **flow** to attach the integration to. See [Flows](/docs/guides/admin/flows/). - Access to the Gaard connector settings in your Azursoft platform. ## Set up the integration 1. In **Settings → Platform → Integrations**, select **Azursoft**. 2. Choose the **Flow** the key should authenticate against. 3. Enter a **Name** that identifies the Azursoft installation. 4. Copy the generated **API Key** and store it securely: treat it like a password. 5. Save. 6. In your Azursoft platform, paste the key into the Gaard connector configuration. ## Verify it 1. Trigger an alarm in Azursoft so it submits a clip to Gaard. 2. In the Gaard workspace, confirm a classification appears on the flow. 3. In Azursoft, confirm the verdict is shown on the event. If nothing appears, check that the integration status is `active` in Gaard and that the key configured in Azursoft matches the one on the integration. ## Related --- # Evalink Integration URL: https://gaard.ai/docs/guides/integrations/alarm-platforms/evalink > Create a follow-up alarm in Evalink that carries the Gaard classification outcome, linked to the original alarm. [Evalink](https://www.evalink.io) is an alarm-management platform. The Gaard integration closes the loop on an alarm that already exists in Evalink: it reads the original alarm referenced in the classification's metadata and, when classification completes, creates a **follow-up alarm** in Evalink carrying the outcome: an actionable alarm when the clip is risky, or a false-alarm code when it is `safe`. The follow-up is linked back to the original alarm and to the clip in Gaard, so an operator sees the AI verification alongside the event that triggered it. This page is part of [Alarm Platforms](/docs/guides/integrations/alarm-platforms/). ## How it works For Gaard to match the original alarm, each classification you send must carry two fields in its `metadata`: - `source`: the alarm source (Evalink). - `alarm_id`: the identifier of the original Evalink alarm. When the classification finishes, Gaard reads the original alarm from Evalink, creates a new alarm that references it, and marks the new alarm's source as Gaard so it is never re-classified in a loop. If the integration is inactive, or the referenced alarm is already sourced from Gaard, no follow-up is created. ## Prerequisites - An **admin** role in the tenant. - A **flow** to attach the integration to. See [Flows](/docs/guides/admin/flows/). - An Evalink **API key**. - Classifications on the flow that carry `source` and `alarm_id` in their metadata. ## Set up the integration 1. In **Settings → Platform → Integrations**, select **Evalink**. 2. Choose the **Flow**. 3. Enter a **Name**. 4. Paste the **API Key** issued by Evalink. 5. Save. :::note The Evalink API endpoint is provisioned on the Gaard side, not in this form: the form captures only the name and API key. Contact your Gaard representative to complete the connection. ::: ## Verify it 1. Trigger a classification on the flow, with `source` and `alarm_id` set in the request metadata so Gaard can find the original alarm. 2. In Evalink, confirm a new alarm appears, sourced from Gaard, referencing the original alarm and linking to the clip in Gaard. If nothing appears, check that the integration status is `active`, the API key is valid, and the metadata carried a matching `alarm_id`. ## Related --- # Prysm Integration URL: https://gaard.ai/docs/guides/integrations/alarm-platforms/prysm > Post an annotated clip and event metadata to a Prysm webhook when a classification completes. [Prysm](https://prysm.fr) (ESI Vision) is an alarm-monitoring platform. When a classification completes, Gaard renders an **annotated video** (the highlight clip with the model's detections drawn on it as bounding boxes) and posts it to your Prysm endpoint together with the event identifiers, so an operator sees a verified, annotated clip in their monitoring view. This page is part of [Alarm Platforms](/docs/guides/integrations/alarm-platforms/). ## What Prysm receives For each completed classification on the flow, Gaard sends one HTTP `POST` to your Prysm endpoint: - The **annotated clip** as `multipart/form-data`. - Your **API key** as a bearer token in the `Authorization` header. - The event identifiers (**site**, **camera**, **event type**, **file**, and **classification**) sent as both HTTP headers and query parameters, so a receiver can read them either way. Your endpoint should acknowledge with `200 OK`. :::caution Prysm delivery is best-effort: Gaard sends one request per configured Prysm endpoint, and a failed request is logged rather than retried. If you configure more than one Prysm integration on the flow, each receives its own request. ::: ## Prerequisites - An **admin** role in the tenant. - A **flow** to attach the integration to. See [Flows](/docs/guides/admin/flows/). - The **URL** of your Prysm event endpoint. - A Prysm **API key**. ## Set up the integration 1. In **Settings → Platform → Integrations**, select **Prysm**. 2. Choose the **Flow**. 3. Enter a **Name**. 4. Set the **URL** of your Prysm event endpoint (for example, `https:///event`). 5. Paste the **API Key** Prysm issued: Gaard sends it as a bearer token. 6. Save. ## Verify it 1. Trigger a classification on the flow. 2. Confirm your Prysm endpoint receives a `POST` carrying the annotated clip and the event identifiers, and that it responds `200 OK`. If nothing arrives, check that the integration status is `active`, the URL is reachable from Gaard, and the API key is correct. ## Related --- # SIA DC-09 Integration URL: https://gaard.ai/docs/guides/integrations/alarm-platforms/sia-dc09 > Send classification events to a central station receiver, or receive panel alarm events and classify them, over SIA DC-09. SIA DC-09 is the ANSI/SIA standard for reporting alarm events over IP, between alarm panels (premises equipment) and central station receivers. Gaard speaks it over TCP, with a CRC on every frame and optional AES-CBC encryption, and supports **both directions**: each as its own per-flow integration. This page is part of [Alarm Platforms](/docs/guides/integrations/alarm-platforms/). :::note The two-letter SIA event codes Gaard uses are a **custom per-customer convention** (for example, Securitas or Prysm/ESI), not canonical SIA DC-03 codes. Confirm the code profile with your monitoring provider. ::: ## Outbound: send events to a receiver When a clip classifies as a reportable event, Gaard acts as premises equipment and emits a DC-09 event over TCP to your Central Station Receiver (CSR), carrying the clip as an extended-data verification link. ```mermaid graph LR C("Reportable classification") --> Q("Durable queue") Q --> G("Gaard DC-09 sender") G -->|"DC-09 event (TCP)"| R("Central Station Receiver") R -.->|"ACK / NAK / DUH"| G ``` Delivery is designed so that no alarm is lost: - The event is written to a durable queue before it is sent, with a per-account sequence number, so a restart mid-delivery resumes cleanly. - On **ACK**, the event is marked delivered. - On **NAK**, Gaard adopts the receiver's clock, corrects the timestamp, and retries. - On **DUH**, the event is treated as a permanent rejection and marked failed. - On **no response**, Gaard retries (by default every 20 seconds, up to 3 attempts) then flags the event as failed. ### Configure outbound 1. In **Settings → Platform → Integrations**, select **SIA DC-09 Outbound**. 2. Choose the **Flow** and enter a **Name**. 3. On the **Connection** tab, set the **Receiver host** and **Receiver port**, the DC-09 identity (**Receiver number (R)**, **Line prefix (L)**, **Account number (#)**) and the **Encryption** policy with its **AES key (hex)** if encrypted. 4. On the **Events** tab, set the **Code profile**, the **Forwarded codes** (which SIA codes are actually sent), and whether to **Include verification link** or **Include JSON payload**. 5. On the **Delivery** tab, set the **Retry timeout (seconds)** and **Max attempts**. 6. Save. ## Inbound: receive and classify panel events In the other direction, Gaard acts as a central station receiver: it runs a long-lived TCP listener, accepts DC-09 alarm events from panels, resolves the reporting zone to a camera, acquires a clip, and classifies it. ```mermaid graph LR P("Alarm panel") -->|"DC-09 event (TCP)"| L("Gaard DC-09 listener") L -.->|"ACK / NAK / DUH"| P L --> Z("Resolve zone to camera") Z --> CL("Acquire clip") CL --> CF("Classify") ``` Each incoming event is validated (CRC, decryption, and an anti-replay timestamp check) and answered per the standard: | Situation | Reply | Result | | --- | --- | --- | | Valid event for an owned account | `ACK` | Zone resolved, clip acquired, classified. | | Unknown account | `DUH` | Rejected. | | Mapped account, unmapped zone | `ACK` | Accepted, but no camera to classify. | | Timestamp outside the anti-replay window | `NAK` | The panel can correct its clock and resend. | | Bad CRC or undecryptable frame | *(none)* | Silently dropped, as the standard requires. | To obtain footage for a valid event, Gaard tries the **clip sources** you configure, in order, until one yields imagery: - `verification-link`: a link carried in the DC-09 event. - `recent-segments`: recently recorded footage for the camera. - `on-demand-grab`: a fresh grab from the camera. ### Configure inbound 1. In **Settings → Platform → Integrations**, select **SIA DC-09 Inbound**. 2. Choose the **Flow** and enter a **Name**. 3. On the **Listener** tab, set the **Listen port** and the **Account numbers** this receiver owns. 4. On the **Zones & clips** tab, define the **Zone → camera map** (which panel zone maps to which camera) and the ordered **Clip sources**. 5. On the **Security** tab, set the **Encryption** policy, the **AES key (hex)**, and the anti-replay timestamp windows. 6. Save. :::caution The inbound receiver opens a raw TCP listener on a dedicated port, separate from the HTTP API. Exposing that port to your alarm network requires firewall and port provisioning: coordinate with Gaard to allocate and open the listener port for your tenant. ::: ## Encryption Both directions support plaintext or **AES-128-CBC**. When you enable encryption, set the same hex key on the Gaard integration and on the peer (the receiver for outbound, the panel for inbound); a key mismatch makes frames undecryptable, and undecryptable inbound frames are dropped. ## Related --- # Alarm Platform Integrations URL: https://gaard.ai/docs/guides/integrations/alarm-platforms > Connect Gaard to Azursoft, Evalink, Prysm, and SIA DC-09 so classification outcomes reach your monitoring stack. Alarm-platform integrations send classification outcomes to the monitoring systems your operators already use, rather than to a generic endpoint. Reach for them when your goal is to raise or enrich an alarm: if you only need the raw result, see [Choosing a Delivery Method](/docs/guides/integrations/choosing-delivery/) instead. Gaard supports four alarm platforms. All are configured from **Settings → Platform → Integrations** and scoped to a flow. ```mermaid graph LR AZI("Azursoft") --> G("Gaard") EVI("Evalink") --> G PRI("Prysm") --> G PANEL("Alarm panel") -->|"DC-09 event"| G G --> AZO("Azursoft") G --> EVO("Evalink") G --> PRO("Prysm") G -->|"DC-09 event"| CSR("Central Station Receiver") ``` ## Compare the platforms | Platform | Direction | Protocol | Typical use | | --- | --- | --- | --- | | **Azursoft** | Inbound and outbound | Gaard classify API (HTTPS) | Native Gaard verification inside Azursoft supervision: Azursoft submits clips and shows the verdict to operators. | | **Evalink** | Inbound and outbound | HTTPS alarm API | Add AI verification to an existing Evalink alarm by creating a linked follow-up alarm. | | **Prysm** | Inbound and outbound | HTTPS webhook (annotated video) | Push a verified, annotated clip to Prysm (ESI Vision) monitoring. | | **SIA DC-09** | Inbound and outbound | SIA DC-09 over TCP | Interoperate with alarm panels and central stations over the standard alarm protocol. | ## Pick a platform ## Prerequisites Every alarm-platform integration needs: - An **admin** role in the tenant. - A **flow** to attach the integration to. See [Flows](/docs/guides/admin/flows/). - Credentials and endpoint details from the platform you are connecting: each platform page lists exactly what to gather. ## Related --- # Choosing a Delivery Method URL: https://gaard.ai/docs/guides/integrations/choosing-delivery > Polling, webhooks, or SFTP: pick the right way to receive classification results. Classification runs asynchronously: you submit a video, Gaard processes it, and the result becomes available a little later. This page explains the three ways to get that result out of Gaard so you can pick the one that fits your architecture. There are two models: - **You pull** the result when you are ready: **polling**. - **Gaard pushes** the result to you as soon as it is ready: **webhooks** or **SFTP delivery**. Each result (risk level, labels, scores, and the annotated highlight) is tied to a `classify_id`. Whichever method you choose, you are receiving the same underlying result. ```mermaid graph TD G("Classification completes") --> R("Result stored against classify_id") R -->|"you pull: GET /api/result"| P("Your poller") R -->|"Gaard pushes: HTTP POST"| W("Your webhook endpoint") R -->|"Gaard pushes: file write"| F("Your SFTP/FTP server") ``` ## Polling Polling is the simplest option: after submitting a video, you call `GET /api/result/` until the status is `done`. - **You own the timing.** Nothing has to be reachable from the internet: your system only makes outbound HTTPS requests. - **Same payload everywhere.** The JSON returned by `GET /api/result/` is the same shape as a synchronous classify (`POST /api/classify?sync=true`) and the same shape webhooks send, so one parser covers every method. - **The trade-off is latency and traffic.** You learn the result is ready only on your next poll, and a short poll interval means many requests that return "not done yet." Polling is a good first integration and the right choice when inbound callbacks are not possible in your environment. See [Endpoints](/docs/endpoints/) for the request and response details. ## Webhooks A webhook flips the direction: when a classification completes, Gaard sends an HTTP `POST` to an endpoint you run, carrying the result JSON. - **Lowest latency.** Delivery is tied to completion: there is no poll interval to wait through. - **You run a receiver.** You need an HTTPS endpoint that Gaard can reach. - **Delivery is best-effort.** Gaard sends one `POST` per configured endpoint, does not retry, does not treat a non-`2xx` response as a retryable failure, and does not add a signature or authentication header. Protect your endpoint at the edge and make your handler idempotent. Webhooks are configured per flow. For setup, the payload, and receiver recommendations, see [Webhooks](/docs/webhooks/). ## SFTP delivery SFTP delivery writes the result to a server you control as files, rather than as an HTTP call. As each classification on the flow completes, Gaard connects to your SFTP/FTP server and drops either the annotated video or the snapshot images. - **File-based, not endpoint-based.** You run an SFTP/FTP server instead of an HTTP receiver: a natural fit for video-management systems and batch or offline consumers. - **Files, not JSON.** What lands on the server is media (an `.mp4` clip or JPEG snapshots), not the result JSON. Reach for polling or webhooks if you need the structured result. - **Delivery is best-effort.** A failed transfer is logged, not retried. For configuration and the exact files that are written, see [SFTP Delivery](/docs/guides/integrations/sftp/). :::tip If your goal is to raise an alarm in a monitoring platform your operators already use (rather than to move raw results) see [Alarm Platforms](/docs/guides/integrations/alarm-platforms/) for Azursoft, Evalink, Prysm, and SIA DC-09. ::: ## Comparison | | Polling | Webhooks | SFTP delivery | | --- | --- | --- | --- | | Direction | You pull | Gaard pushes | Gaard pushes | | Transport | HTTPS `GET` | HTTPS `POST` | SFTP/FTP file write | | Latency | Depends on your poll interval | Near real-time | Near real-time | | You must run | Nothing inbound | An HTTPS endpoint | An SFTP/FTP server | | You receive | Result JSON | Result JSON | Video or snapshot files | | Delivery guarantee | On demand, as reliable as your polling | Best-effort, one attempt, no retries | Best-effort file write | | Scope | Per request | Per flow | Per flow | | Best for | A first integration, or restricted networks | Event-driven pipelines | File-based, VMS, and batch consumers | ## Choosing - **Start with polling** if you are integrating for the first time, cannot accept inbound connections, or want explicit control over when you fetch results. - **Move to webhooks** once you operate an HTTP service and want near-real-time, event-driven processing without repeated polling. - **Use SFTP delivery** when your consumer is file-based: a video-management system, an evidence archive, or a batch job that ingests clips and images rather than JSON. - **Combine them.** You can poll and configure webhooks at the same time, and webhook delivery does not disable `GET /api/result/`. Configuring multiple destinations is how you fan a result out to several systems. ## Next steps - [Webhooks](/docs/webhooks/): set up push delivery and design a reliable receiver. - [SFTP Delivery](/docs/guides/integrations/sftp/): deliver results as files to your own server. - [Alarm Platforms](/docs/guides/integrations/alarm-platforms/): connect Azursoft, Evalink, Prysm, and SIA DC-09. --- # SFTP Delivery URL: https://gaard.ai/docs/guides/integrations/sftp > Deliver classification results as files over SFTP or FTP, and control delivery with a persistent pause. SFTP delivery pushes classification results to a server you control, as files. As each classification on a flow completes, Gaard connects to your SFTP/FTP server and drops either the annotated video or the snapshot images. It is the right delivery method for file-based consumers (video-management systems, evidence archives, and batch jobs) that ingest media rather than JSON. For how this compares to polling and webhooks, see [Choosing a Delivery Method](/docs/guides/integrations/choosing-delivery/). ## Prerequisites - An **admin** role in the tenant. - A **flow** to attach the integration to. See [Flows](/docs/guides/admin/flows/). - An SFTP or FTP server reachable from Gaard, with credentials (a username and password, or an SSH private key for SFTP). ## Add an SFTP delivery integration SFTP delivery is the **SFTP/FTP Outbound** integration, configured per flow. 1. In the web app, open **Settings → Platform → Integrations**. 2. Under **Add a new integration**, select **SFTP/FTP Outbound**. 3. Choose the **Flow** whose results you want to deliver. 4. Enter a **Name** for the integration. 5. Set the **File Protocol**: `FTP` or `SFTP`. 6. Enter the **Host** and **Port**. The port defaults to `21`; set it to your SFTP port (typically `22`) when the protocol is `SFTP`. 7. Enter the **Username** and **Password**. When the protocol is `SFTP`, you can supply a **Private Key** instead of a password. 8. Choose what to send with **Send Snapshots** (see [What lands on your server](#what-lands-on-your-server) below). 9. Optionally enable **Clean up SFTP folder after 15 minutes** to have Gaard remove delivered files 15 minutes after writing them. 10. Select **Test** to verify the connection, then save. :::note The **Private Key** field only appears when the protocol is `SFTP`. FTP connections authenticate with a username and password. ::: If you need to deliver the same results to more than one server, add one **SFTP/FTP Outbound** integration per destination. ## What lands on your server What Gaard writes depends on the **Send Snapshots** toggle: | Send Snapshots | Files written | | --- | --- | | Off (default) | The classification's highlight clip, written as a single `.mp4` file. | | On | The JPEG snapshots (`.jpg` / `.jpeg`) for the alert, written as individual image files. | Files are written into the working directory your account lands in when it connects. Delivery happens automatically for every completed classification on the flow: there is no separate scheduled batch window. :::caution File delivery is best-effort. A failed transfer is logged but not retried. If you need the structured result JSON rather than media files, use [polling or webhooks](/docs/guides/integrations/choosing-delivery/) instead. ::: ## Test the connection The **Test** button opens a connection with the credentials in the form and confirms Gaard can reach the server and read its working directory. The check times out after a few seconds, so a failing test points to an unreachable host, a wrong port, or bad credentials. Fix those before saving. ## Monitor delivery status Each integration in the list carries a status indicator: | Status | Meaning | | --- | --- | | `active` | The integration is running normally. | | `paused` | You paused the integration (see below). | | `error` | The last connection failed: the error message is shown on the row. | ## Inbound SFTP retrieval and the persistent pause Gaard can also work in the other direction: the **SFTP/FTP** integration *retrieves* files from an SFTP/FTP server, groups them into alerts, and classifies them. Unlike outbound delivery (which fires once per classification) inbound retrieval runs a live watcher that continuously polls the server. Because that watcher is a running connection, you can pause and resume it: 1. In **Settings → Platform → Integrations**, open the row menu for the SFTP/FTP integration. 2. Select **Stop** to pause retrieval, or **Start** to resume it. The pause is **persistent**: a paused integration stays paused across restarts until you start it again, and its status shows `paused`. This is the safe way to take a retrieval connection offline for maintenance without deleting its configuration. Global tuning for inbound retrieval (the poll interval, how files are grouped into alerts, and file-age handling) lives in **Settings → Platform → Configuration → SFTP Integration**. ## Next steps --- # What is Gaard? URL: https://gaard.ai/docs/guides/overview > The Gaard platform in five minutes: solutions, surfaces, and how a video becomes a decision. Gaard turns surveillance video into decisions. This page gives you the five-minute picture of what the platform does, how it is organized, and how you interact with it. ## The problem Gaard solves Security and surveillance operations generate far more video than any team can watch. A single site can trigger thousands of motion alarms a day, the overwhelming majority of them harmless: wind in a tree, rain on a lens, an animal crossing a car park. Operators drown in false alarms, and the one event that matters is buried in the noise. Gaard applies AI to that stream. It watches each clip, scores what it sees, and assigns a **risk level** so operators can spend their attention on the alarms that are actually suspicious. Harmless activity is filtered down; genuine intrusions are surfaced and escalated. ## Two solutions Gaard is sold as two entitleable solutions. Your account may include one or both. | Solution | What it delivers | | --- | --- | | **Classify** | AI video classification. Each clip is analyzed and assigned a risk level (`safe`, `danger`, or `intrusion`), a set of labels, and an annotated highlight showing what the model reacted to. This is the focus of these guides. | | **Replay** | A cloud-based network video recorder (NVR) for recording and reviewing historical footage. | :::note These guides document **Classify**. Replay is available separately and is not yet covered in the public documentation. ::: The rest of this page (and most of the documentation) is about Classify. ## How you interact with Gaard Classify exposes the same data and the same vocabulary through three surfaces. Pick whichever fits the task. | Surface | Where | Use it to | | --- | --- | --- | | **Web app** | [app.gaard.ai](https://app.gaard.ai) | Review classifications, manage sites and cameras, configure the platform, and administer users. Start with the [Web App Quickstart](/docs/guides/quickstart-webapp/). | | **REST API** | `https://vision.gaard.ai/api` | Submit video for classification and retrieve results programmatically. Start with the [API Quickstart](/docs/getting-started/). | | **Outbound integrations** | Your systems | Receive results automatically over webhooks, SFTP, or an alarm platform. See [Webhooks](/docs/webhooks/). | All three read and write the same tenant. A clip submitted through the API appears in the web app, and a result delivered to your systems matches what an operator sees. ## From video to decision Every clip follows the same journey, whichever surface starts it. ```mermaid graph TB subgraph ingest ["Ingest"] U("Web app upload") A("POST /api/classify") C("Connected camera") end ingest --> M("AI classification") M -->|"intrusion score"| R("Risk level") R --> O("Operator review") R -->|"webhook · SFTP · alarm platform"| S("Your systems") ``` 1. **Ingest.** A video clip enters Gaard: uploaded in the web app, posted to `POST /api/classify`, or captured from a connected camera. Optional metadata identifies the site and camera it came from. 2. **Classification.** The AI model analyzes the clip and produces a set of confidence **scores** between `0.0` and `1.0`: one per concept it looks for (`intrusion`, `person`, `vehicule`, `animal`, and more). 3. **Risk level.** Gaard derives a single **risk level** from the `intrusion` score using two configurable thresholds: below the low threshold is `safe`, between the two is `danger`, above the high threshold is `intrusion`. See [Risk, labels, and scores](/docs/risk-labels-scores/). 4. **Review.** Operators open the classification in the web app, play the annotated clip, and confirm or correct the model's decision. `safe` clips can be filtered out; `danger` and `intrusion` always stay visible. 5. **Delivery.** The [result](/docs/response-structure/) (status, risk level, labels, scores, and an annotated highlight) is available immediately over the API and can be pushed to your own systems by webhook, SFTP, or alarm platform. The result of that journey is a small, structured decision instead of an hour of footage: *this clip, from this camera, is an intrusion; here is the highlight.* ## Next steps - [Web App Quickstart](/docs/guides/quickstart-webapp/): sign in and review your first classification. - [Core Concepts](/docs/guides/concepts/): the vocabulary shared across every surface. - [Risk, labels, and scores](/docs/risk-labels-scores/): how a score becomes a risk level. --- # Quickstart: Web App URL: https://gaard.ai/docs/guides/quickstart-webapp > Sign in, explore the dashboard, and review your first classification. This tutorial walks you through your first session in the Gaard web app: from signing in to reviewing your first classified clip. By the end, you will know where classifications live and how to read one. ## Prerequisites - A Gaard account with access to a tenant that has the **Classify** solution. - Some classified video in that tenant. If it is brand new, submit a clip first with the [API Quickstart](/docs/getting-started/). If you do not have an account yet, ask your administrator to invite you (see [Users & Roles](/docs/guides/admin/users-roles/)). ## Sign in 1. ## Open the app Go to [app.gaard.ai](https://app.gaard.ai). You land on the **Sign in to your account** screen. 2. ## Enter your credentials Type your **Email address** and **Password**, then select **Sign in**. Prefer not to use a password? Select **Magic link** under **Or continue with**, enter your email, and select **Send magic link**. Gaard emails you a one-time sign-in link that expires in 24 hours: open it to sign in. You can also **Sign in with Google** from the same screen. :::tip Forgot your password? Select **Forgot password?** on the sign-in screen to reset it. ::: ## Explore the dashboard After signing in you land on the **Dashboard**: your at-a-glance view of classification activity for the selected time range. The dashboard shows: - **Summary statistics**: **Analyzed alarms**, **Filter rate**, **Analyzed sites**, and **Analyzed cameras** for the current time range. - **A trend chart** of classification volume over time. - **Cameras with most alarms** and **Sites with most alarms**: where activity is concentrated. Use the **time selector** in the top action bar to change the window, and the **flow switcher** next to it to focus the dashboard on a single [flow](/docs/guides/concepts/#flows). The left sidebar is your primary navigation: | Item | Where it takes you | | --- | --- | | **Dashboard** | The activity overview you are on. | | **Classify** | The review workspace: every classification, filterable. | | **Labels** | Manage the labels available in your tenant. | | **Sites** | Your sites and their cameras. | | **Cameras** | All cameras across sites. | | **Upload** | Submit a video clip for classification from the browser. | :::note The exact items you see depend on your role and entitlements: administrators see more than operators. See [Users & Roles](/docs/guides/admin/users-roles/). ::: ## Review your first classification 1. ## Open the review workspace Select **Classify** in the sidebar. This is the review workspace: a filterable grid of every classification in the current flow and time range, above a chart of activity. Each row shows the **Alarm**, its **Risk**, a **Video** thumbnail, the **Classify Time**, and the **Date**. Use the filter bar at the top (**Filter alarms…**) to narrow the list: for example, to a single camera or risk level. 2. ## Open a clip Select any row to open its classification. The clip plays with the model's annotated highlight overlaid, so you can see what the model reacted to. 3. ## Read the risk and scores Alongside the clip, the **Scores** tab lists the model's confidence per concept. The **Risk** shown on the row (`safe`, `danger`, or `intrusion`) is derived from the `intrusion` score using your tenant's thresholds. See [Risk, labels, and scores](/docs/risk-labels-scores/) for how that derivation works. That is the core loop: an operator opens a clip, watches the annotated highlight, and confirms or corrects the decision. `safe` clips can be filtered away; `danger` and `intrusion` stay visible until someone reviews them. ## Next steps - [Reviewing Classifications](/docs/guides/classify/reviewing/): the full review workspace, tab by tab. - [Labeling & Feedback](/docs/guides/classify/labeling-feedback/): tell Gaard when it was right or wrong. - [Core Concepts](/docs/guides/concepts/): the vocabulary behind everything you just saw. --- # Gaard URL: https://gaard.ai/docs/index > Comprehensive technical documentation for Gaard. ## Getting started New to Gaard? Start here: what the platform does, how to sign in, and the vocabulary used everywhere else. ## Classify Day-to-day work with AI video classification: submitting clips, reviewing results, and improving the model with feedback. ## Administration Manage your tenant: people, credentials, platform behavior, and the flows that organize incoming video. ## Integrations & delivery Get classification results into your own systems: pick a delivery method and connect your alarm platform. ## Deployment & trust Where Gaard runs and how your data is protected. ## API reference Complete reference for the Gaard REST API: endpoints, response schemas, webhooks, and scoring. --- # Labels API URL: https://gaard.ai/docs/labels > List available labels and post feedback labels on classifications. Labels let you provide feedback on classification results: marking them as true positives, false positives, or applying custom labels for your own tracking. ## List all labels ``` OPTIONS /api/label ``` Returns the standard and custom labels available for the current tenant. ### Example response ```json { "standard": [ "animal", "flag", "person", "plant", "rain", "spider", "text", "vehicle", "web", "wind", "EMPTY", "FIX", "N/A" ], "custom": ["my-custom-label"] } ``` | Field | Type | Description | | --- | --- | --- | | `standard` | string[] | Built-in labels provided by Gaard. | | `custom` | string[] | Custom labels created by your team. | :::note This endpoint uses the `OPTIONS` HTTP method. Some HTTP clients default to `GET`: make sure yours supports `OPTIONS` requests. ::: ## Post a label ``` POST /api/label/ ``` Attaches one or more labels and an optional comment to an existing classification result. ### Request body ```json { "labels": ["FP"], "comment": "False alarm - wind moving a branch" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | `labels` | string[] | Yes | One or more label values to attach. | | `comment` | string | No | Free-text comment explaining the label. | ### Recommended labels | Label | Meaning | | --- | --- | | `FP` | False positive: Gaard flagged the video but there was no real threat. | | `TP` | True positive: Gaard correctly identified a threat. | | `FN` | False negative: Gaard missed a real threat. | | `TN` | True negative: Gaard correctly classified the video as safe. | You can also use simpler labels: `true` or `false`. Custom labels are accepted: any string value you provide will be stored and will appear in the list returned by `OPTIONS /api/label`. --- # Metadata URL: https://gaard.ai/docs/metadata > Optional metadata.json format for Gaard API video submissions. The `metadata.json` file is a flexible JSON structure that clients can send alongside the video file. It enhances the context of the video data and supports more tailored processing and analysis. **All fields are optional.** ## Example ```json { "site_id": 134188, "camera_id": "VI08" } ``` ## Fields | Field | Type | Description | | --- | --- | --- | | `site_id` | string \| int | Site identifier. Falls back to `client_id` if not provided. | | `camera_id` | string \| int | Camera identifier. Falls back to `code_msg` if not provided. | ## Usage notes - While all fields are optional, providing `site_id` and `camera_id` enhances data identification and processing. - The metadata format is flexible: clients can include additional fields specific to their needs. - The entire metadata object is included in the [classification result](/docs/classification-result/), ensuring a comprehensive overview of the analysis context. --- # API v3.0 URL: https://gaard.ai/docs/overview > Gaard API v3.0 reference documentation: endpoints, response structures, classification results, and configuration. API v3.0 is a significant step toward a unified and flexible API. Full backward compatibility is maintained across API versions. These docs describe the latest version by default, but previous API versions remain supported. If you need documentation for an older API version, contact support. ## Base URL ```bash frame="terminal" title="terminal" https://vision.gaard.ai/api ``` ## Authentication All API requests must include a valid token in the `Authorization` header using the Bearer scheme: ``` Authorization: Bearer ``` API tokens are created in the Gaard app at [Settings > Platform > Integrations](https://app.gaard.ai/settings/platform/integrations). Each token is scoped to a tenant and optionally to a specific flow. Requests with a missing or invalid token receive a `403` response: ```json { "success": false, "error": "no access to current endpoint, make sure you are using correct API key" } ``` ## Sections - [Endpoints](/docs/endpoints/): classify, retrieve results, download annotated video, delete - [Response structure](/docs/response-structure/): full response schema and field descriptions - [Classification result](/docs/classification-result/): detailed result type, status constants, and examples - [Webhooks](/docs/webhooks/): configure callback delivery, inspect the payload, and understand webhook latency - [Metadata](/docs/metadata/): optional `metadata.json` format for video submissions - [Risk, labels, and scores](/docs/risk-labels-scores/): how scores map to risk levels and labels - [Labels API](/docs/labels/): list labels, post feedback labels on classifications --- # Response structure URL: https://gaard.ai/docs/response-structure > Full response schema returned by the Gaard API classify and result endpoints. ## Endpoints - `POST /api/classify?sync=true`: returns the full result immediately - `GET /api/result/{id}`: returns the result for a given classification ## Example response ```json { "id": "664371916d24ab9cf81140ec", "status": { "classify": "done", "video": "done" }, "parent_id": "000000000000000000000000", "camera_id": "134188-VI08", "analyse_id": 3373550353, "tenant": "tenant", "duration": 2968236, "duration_seconds": 2, "model": "noname", "version": "2.0.16123", "error_code": 0, "error_msg": "", "risk": "intrusion", "labels": ["intrusion", "person"], "scores": { "flag": 0.049, "plant": 0.030, "web": 0.009, "NOTHING": 0.0005, "intrusion": 0.973, "person": 0.973, "rain": 0.002, "spider": 0.007, "text": 0.0006, "wind": 0.021, "animal": 0.035, "other": 0.028, "vehicule": 0.101 }, "video": { "videoname": "video.mov", "filesize": 786800, "specs": { "height": 320, "original.width": 640, "duration": 4.217772, "fps": 3.08, "nframes": 13, "original.fps": 3, "original.height": 360, "original.nframes": 12, "width": 568 } }, "timing": { "total": 2593.50306 }, "metadata": { "no_trans": "20818", "parc_origine": "RO", "camera_id": 134188 }, "created_at": "2024-05-14T16:13:37.156+02:00", "started_at": "2024-05-14T16:13:37.156+02:00" } ``` ## Field reference | Field | Type | Description | | --- | --- | --- | | `id` | string | Unique classification identifier (`classify_id`). | | `status` | object | Status of the classification and video processing. | | `parent_id` | string | Identifier of the parent object. | | `camera_id` | string | Camera identifier, derived from metadata. | | `analyse_id` | int | Analysis identifier. | | `tenant` | string | Tenant identifier (mandatory). | | `duration` | int | Classification duration in milliseconds. | | `duration_seconds` | int | Classification duration in seconds. | | `model` | string | Model used for classification. | | `version` | string | Model version. | | `error_code` | int | Error code (0 if no error). | | `error_msg` | string | Error message (empty if no error). | | `risk` | string | Risk level: `safe`, `danger`, or `intrusion`. | | `labels` | string[] | Labels assigned by the analysis. | | `scores` | object | Confidence scores for each label (0.0–1.0). | | `video` | object | Video file details (name, size, specs). | | `timing` | object | Processing timing details. | | `metadata` | object | Optional metadata submitted with the video. | | `created_at` | string | ISO 8601 timestamp when the analysis was created. | | `started_at` | string | ISO 8601 timestamp when the analysis started. | :::note Metadata is optional. No specific format is required unless the client wants to provide `camera_id` and `site_id`. See [Metadata](/docs/metadata/). ::: --- # Risk, labels, and scores URL: https://gaard.ai/docs/risk-labels-scores > How Gaard derives risk levels and labels from classification scores. ## Scores The `scores` object contains the raw output probabilities from the classification model. Each score is a floating-point value between **0.0** and **1.0**, representing the model's confidence that a given concept is present in the analyzed video segment. ```json "scores": { "intrusion": 0.919, "person": 0.127, "vehicule": 0.893, "animal": 0.062, "flag": 0.830, "plant": 0.049, "rain": 0.005, "wind": 0.007, "text": 0.001, "other": 0.020, "NOTHING": 0.0003 } ``` :::caution While multiple scores are exposed for observability and debugging, **risk evaluation is based exclusively on the `intrusion` score**. ::: ## Risk levels The system exposes **three and only three** risk levels: - `safe` - `danger` - `intrusion` These are **directly derived from the `intrusion` score** using two configurable thresholds. ### Thresholds | Threshold | Description | | --- | --- | | Low threshold | Boundary between `safe` and `danger` | | High threshold | Boundary between `danger` and `intrusion` | Typical configuration: ``` low_threshold = 0.2 high_threshold = 0.8 ``` See [Platform Configuration](/docs/guides/admin/platform-configuration/) to customize thresholds. ## Risk derivation | Intrusion score | Risk level | Meaning | | --- | --- | --- | | `< low_threshold` | **safe** | No action required | | `>= low_threshold` and `< high_threshold` | **danger** | Requires operator validation | | `>= high_threshold` | **intrusion** | Confirmed intrusion | ```go if intrusion >= highThreshold { risk = "intrusion" } else if intrusion >= lowThreshold { risk = "danger" } else { risk = "safe" } ``` ## Operational semantics - **Safe**: Normal background activity. May be automatically filtered out. - **Danger**: Ambiguous or suspicious activity. **Must be reviewed by an operator.** Never automatically discarded. - **Intrusion**: High-confidence intrusion detected. **Must be reviewed and escalated.** Never automatically discarded. :::note[Design principle] Everything that is not `safe` (`danger` and `intrusion`) **must remain visible to operators** and must not be filtered out automatically. ::: --- # Webhooks URL: https://gaard.ai/docs/webhooks > Configure Gaard webhooks, understand when they fire, inspect the payload, and design a reliable receiver. Gaard webhooks let you receive classification results as an HTTP `POST` to your own endpoint as soon as processing is finished. For integrators, the main idea is simple: - your system sends a video to Gaard - Gaard classifies it asynchronously - when the final result is available, Gaard posts the result JSON to your webhook URL ```mermaid graph TD subgraph pipeline ["Processing Pipeline"] direction LR B("Gaard API") --> C("Classification pipeline") --> D("Result stored") end A("Your system") -->|"POST /api/classify"| B D -->|"Webhook POST (JSON)"| E("Your webhook endpoint") E --> F("Your queue, incident system, or business workflow") ``` ## What a webhook sends Gaard currently sends one webhook event type: the final classification result. The webhook payload uses the same JSON shape as the result returned by: - `GET /api/result/{id}` - `POST /api/classify?sync=true` That means you can usually reuse the same parser for both polling and webhook-based integrations. ## How it works 1. Your application submits a video to `POST /api/classify`. 2. Gaard accepts the job and processes it asynchronously. 3. After classification is completed and the result is persisted, Gaard sends an HTTP `POST` request to each webhook configured for the flow. 4. Your endpoint receives the result payload and can trigger downstream actions such as incident creation, alarm enrichment, or archival. ## Set up a webhook in Gaard In the Gaard application: 1. Open the integrations section. Platform integrations 2. Create a new `Webhook` integration. 3. Select the flow that should trigger the webhook. 4. Enter the destination URL of your receiver. 5. Save the integration. 6. Submit a test classification and verify that your endpoint receives the payload. If you need to send the same result to multiple systems, create multiple webhook integrations. ## HTTP request format Gaard sends: - Method: `POST` - Header: `Content-Type: application/json` - Body: classification result JSON Gaard does not currently add custom authentication headers or a signature header. If your receiver requires authentication, protect it at your edge, for example with a secret URL path, a reverse proxy, or another mechanism you control. ## Example payload ```json frame="terminal" title="Classify WebHook payload" { "id": "6662f5c1f897618de43f0bbd", "status": { "classify": "done", "video": "done" }, "parent_id": "000000000000000000000000", "camera_id": "134188-VI08", "analyse_id": 3373550353, "tenant": "tenant", "duration": 2968236, "duration_seconds": 2, "model": "noname", "version": "2.0.16123", "error_code": 0, "error_msg": "", "risk": "intrusion", "labels": ["intrusion", "person"], "result": "classified", "scores": { "flag": 0.049, "plant": 0.03, "web": 0.009, "NOTHING": 0.0005, "intrusion": 0.973, "person": 0.973, "rain": 0.002, "spider": 0.007, "text": 0.0006, "wind": 0.021, "animal": 0.035, "other": 0.028, "vehicule": 0.101 }, "video": { "videoname": "video.mov", "filename": "video.mov", "filesize": 786800, "specs": { "height": 320, "original.width": 640, "duration": 4.217772, "fps": 3.08, "nframes": 13, "original.fps": 3, "original.height": 360, "original.nframes": 12, "width": 568 } }, "metadata": { "camera_id": "VI08", "site_id": 134188 }, "created_at": "2024-05-14T16:13:37.156+02:00", "started_at": "2024-05-14T16:13:37.156+02:00" } ``` ## Payload field notes | Field | Type | Notes | | --- | --- | --- | | `id` | string | Stable classification identifier. Use it as your primary correlation key. | | `status` | object | Final processing state for classification and video annotation. | | `camera_id` | string | Camera identifier derived from metadata. | | `analyse_id` | int | Original analysis identifier when provided by the sender. | | `tenant` | string | Gaard tenant that produced the result. | | `risk` | string | High-level outcome such as `safe`, `danger`, or `intrusion`. | | `labels` | string[] | Labels derived from the score set. | | `result` | string | Raw engine result string such as `classified`. | | `scores` | object | Per-label confidence scores. | | `video` | object | Video file name, size, and technical specs. | | `metadata` | object | Original metadata submitted with the classification request. | | `created_at` | string | When the classification task was created. | | `started_at` | string | When processing started. | For the full field reference, see [Response structure](/docs/response-structure/) and [Classification result](/docs/classification-result/). ## Latency and delivery behavior Webhook delivery is tied to classification completion. - Gaard does not send the webhook when the job is merely accepted. - Gaard sends the webhook after the classification result is stored and post-processing is complete. - In practice, webhook timing is usually very close to the moment the result becomes available from `GET /api/result/{id}`. There is no separate webhook queue or scheduled batch window. End-to-end webhook latency is therefore: ```text Σ(t) = classify + post-processing + network ``` Gaard does not currently publish a fixed webhook latency SLA, because classification duration depends on the video, model, runtime load, and network distance to your receiver. ## Reliability model Webhook delivery is currently best effort. :::note Gaard webhooks are built with simplicity in mind and work well for common integration flows. If you need a specific delivery feature or reliability guarantee, contact us. ::: - Gaard sends one HTTP `POST` per configured webhook endpoint. - Gaard does not currently implement automatic retries. - Gaard does not currently treat non-`2xx` HTTP responses as retryable delivery failures. Because of that, your receiver should: - accept the request quickly - return a response immediately after basic validation - move heavier work to an internal queue or background worker - store the payload `id` so your processing is idempotent ## Receiver recommendations To make your integration robust: - Accept `application/json` requests. - Validate that `id`, `tenant`, and the fields you depend on are present. - Use the payload `id` as your deduplication key. - Treat `error_code != 0` as a completed result with an error, not as a transport failure. - Keep your webhook endpoint fast and asynchronous. - Log the full payload during your initial rollout so you can confirm which metadata fields are present in your environment. ## Example response from your endpoint Your endpoint can return a minimal success response such as: ```http HTTP/1.1 200 OK Content-Type: application/json {"ok":true} ``` ## When to use webhooks vs polling Use webhooks when: - you want near-real-time downstream processing - you already operate an HTTP service that can receive callbacks - you want to avoid repeated polling for result availability Use polling with `GET /api/result/{id}` when: - inbound HTTP callbacks are not possible in your environment - you need explicit control over fetch timing - you want a simpler first integration before moving to event-driven delivery