Contents
API docs

API reference

Every endpoint: what it takes, what it returns, and the request in curl and Python.

View as Markdown

Generated from the API's own schema, so it cannot drift from what the server accepts. The base URL is https://api.feintsecurity.com/v1, and every request carries an API key as a bearer token; Authentication covers keys and scopes, Errors the two error shapes.

The Python samples assume this setup:

import os
import requests

API = "https://api.feintsecurity.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['FEINT_API_KEY']}"}

The schema itself is at https://api.feintsecurity.com/v1/openapi.json for generators and API clients.

Canary tokens#

Decoy credentials: issue, list, rotate, revoke.

The canary object#

Fields

id
uuid
The canary's id.
provider
string
The platform the credential lives on: gitlab or aws_iam.
name
string
The credential's name at the provider: the token's name at GitLab, or the IAM user's name for an AWS key. Generic on purpose: whoever holds the credential can read it.
placement
string
Where the secret was placed, as you described it. Named in every alert.
metadata
object
Your own notes, as given at issue or edited in the console.
scopes
array of string
The provider's own scopes on the credential, e.g. create_runner. Empty for an AWS key, whose user has no permissions at all.
status
string
active, expired, rotated, revoked or missing. expired is derived from expires_at.
secret_prefix
string
What identifies the secret in a listing: the first 12 characters of a GitLab token, or the whole access key ID of an AWS key, since no part of a secret access key is kept.
secret_fingerprint
string
SHA-256 of the secret (for an AWS key, of the secret access key alone), as hex. Match a found credential against it without holding the secret.
external_id
string
The credential's id at the provider: for aws_iam the access key ID (AKIA…), which is what CloudTrail and GuardDuty show; for gitlab the token's numeric id.
access_level
integer | null
The provider's access level on the credential, where it has one. 10 is a GitLab Guest; null for an AWS key.
expires_at
datetime | null
When the credential stops working: midnight UTC of the expiry date. GitLab enforces it itself; Feint revokes an expired AWS key within minutes.
revoked_at
datetime | null
When the canary was revoked or rotated away. Null while active.
rotated_from_id
uuid | null
The canary this one replaced, when it was issued by rotation.
created_at
datetime
When the canary was issued.
last_used_at
datetime | null
When the credential was last seen used: GitLab's throttled last-used time, or the latest CloudTrail record for an AWS key. Non-null means the canary has fired.
last_checked_at
datetime | null
When Feint's monitor last checked the credential at the provider.
last_used_ips
array of string
Every source address the provider has reported for the credential, accumulated.
acknowledged_at
datetime | null
When someone last acknowledged this canary's use, in the console.
acknowledged_by
string | null
The user id of whoever acknowledged it.
needs_attention
boolean
Used, and not acknowledged since. A fresh use after an acknowledgement sets it again.
client
object | null
MSSP partners: the client this canary belongs to. Null for your own organization's.
The canary object
{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "scopes": ["create_runner"],
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "external_id": "12345678",
  "access_level": 10,
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_checked_at": "2026-09-26T09:19:02.113045Z",
  "last_used_ips": [],
  "needs_attention": false
}

POST/v1/tokens#

Request a canary token

Requires tokens:write

Mint a decoy credential at the provider and record where you will place it. The response is the only time the secret is returned: Feint keeps a prefix and a fingerprint, not the secret.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Request body

provider
string · default "gitlab"
The platform to mint on: gitlab (the default) for a project access token, or aws_iam for an IAM access key in an AWS account Feint owns. Where AWS decoys are not yet enabled, aws_iam is refused with 400.
placementrequired
string · ≤ 200 characters
Where the secret will live, e.g. prod .env on web-01. One line. Every alert names it, so write it for whoever will be reading the alert.
metadata
object
Your own notes, kept with the canary and shown on alerts. At most 20 entries; keys up to 64 characters, string values up to 500.
ttl_days
integer | null · 1–3650
Lifetime in days. Omit for the default of one year. GitLab enforces the expiry itself; Feint revokes an expired AWS key within minutes of midnight UTC.

Returns 201

Returns a canary, plus secret, secret_notice.

secret
string
The credential itself: the GitLab token, or the secret access key of an AWS key, whose access key ID is external_id. Returned here and never again.
secret_notice
string
A reminder, for whoever reads the raw response.

Errors

400Refused: the provider would not do it, or a limit was reached.
401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
422The request could not be read as this endpoint expects.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl -X POST https://api.feintsecurity.com/v1/tokens \
  -H "Authorization: Bearer $FEINT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "placement": "prod .env on web-01",
    "metadata": {"owner": "platform"},
    "ttl_days": 90
  }'
r = requests.post(
    f"{API}/tokens",
    headers=HEADERS,
    json={
        "placement": "prod .env on web-01",
        "metadata": {"owner": "platform"},
        "ttl_days": 90,
    },
)
r.raise_for_status()
canary = r.json()
201 Created
{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "scopes": ["create_runner"],
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "external_id": "12345678",
  "access_level": 10,
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_checked_at": "2026-09-26T09:19:02.113045Z",
  "last_used_ips": [],
  "needs_attention": false,
  "secret": "glpat-Xy7Q2kN4mP8rT1vW6yB3",
  "secret_notice": "This is the only time the secret is returned. Feint does not store it."
}

GET/v1/tokens#

List canary tokens

Requires tokens:read

Every canary the organization owns, retired ones included, with the ones that need attention first.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns an array of canaries.

Errors

401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl https://api.feintsecurity.com/v1/tokens \
  -H "Authorization: Bearer $FEINT_API_KEY"
r = requests.get(f"{API}/tokens", headers=HEADERS)
r.raise_for_status()
canaries = r.json()
200 OK
[
  {
    "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
    "provider": "gitlab",
    "name": "ci-3f9a1b2c4d",
    "placement": "prod .env on web-01",
    "metadata": {"owner": "platform"},
    "scopes": ["create_runner"],
    "status": "active",
    "secret_prefix": "glpat-Xy7Q2k",
    "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
    "external_id": "12345678",
    "access_level": 10,
    "expires_at": "2027-09-26T00:00:00Z",
    "created_at": "2026-09-26T09:14:07.412903Z",
    "last_checked_at": "2026-09-26T09:19:02.113045Z",
    "last_used_ips": [],
    "needs_attention": false
  }
]

GET/v1/tokens/{canary_id}#

Get one canary token

Requires tokens:read

One canary by id. Ids are scoped to the organization, so another tenant's id is a 404, not a 403.

Path parameters

canary_idrequired
string
The canary's id.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns a canary.

Errors

401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl https://api.feintsecurity.com/v1/tokens/$CANARY_ID \
  -H "Authorization: Bearer $FEINT_API_KEY"
canary_id = "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd"

r = requests.get(f"{API}/tokens/{canary_id}", headers=HEADERS)
r.raise_for_status()
canary = r.json()
200 OK
{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "scopes": ["create_runner"],
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "external_id": "12345678",
  "access_level": 10,
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_checked_at": "2026-09-26T09:19:02.113045Z",
  "last_used_ips": [],
  "needs_attention": false
}

DELETE/v1/tokens/{canary_id}#

Revoke a canary token

Requires tokens:write

Revoke the credential at the provider and mark the canary revoked. Revoking twice is harmless. The record stays in the list with its history.

Path parameters

canary_idrequired
string
The canary's id.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns a canary.

Errors

400Refused: the provider would not do it, or a limit was reached.
401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl -X DELETE https://api.feintsecurity.com/v1/tokens/$CANARY_ID \
  -H "Authorization: Bearer $FEINT_API_KEY"
canary_id = "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd"

r = requests.delete(f"{API}/tokens/{canary_id}", headers=HEADERS)
r.raise_for_status()
canary = r.json()
200 OK
{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "scopes": ["create_runner"],
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "external_id": "12345678",
  "access_level": 10,
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_checked_at": "2026-09-26T09:19:02.113045Z",
  "last_used_ips": [],
  "needs_attention": false
}

POST/v1/tokens/{canary_id}/rotate#

Rotate a canary token

Requires tokens:write

Replace the credential, keeping the placement, metadata and original lifetime. The replacement is minted before the old credential is revoked, so a failure leaves you a working canary. The old record becomes rotated and the new one names it in rotated_from_id. Rotation does not count against the plan's canary cap.

Path parameters

canary_idrequired
string
The canary's id.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns a canary, plus secret, secret_notice.

secret
string
The credential itself: the GitLab token, or the secret access key of an AWS key, whose access key ID is external_id. Returned here and never again.
secret_notice
string
A reminder, for whoever reads the raw response.

Errors

400Refused: the provider would not do it, or a limit was reached.
401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl -X POST https://api.feintsecurity.com/v1/tokens/$CANARY_ID/rotate \
  -H "Authorization: Bearer $FEINT_API_KEY"
canary_id = "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd"

r = requests.post(f"{API}/tokens/{canary_id}/rotate", headers=HEADERS)
r.raise_for_status()
canary = r.json()
200 OK
{
  "id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "provider": "gitlab",
  "name": "ci-3f9a1b2c4d",
  "placement": "prod .env on web-01",
  "metadata": {"owner": "platform"},
  "scopes": ["create_runner"],
  "status": "active",
  "secret_prefix": "glpat-Xy7Q2k",
  "secret_fingerprint": "3c1a9e5f7b2d4a6c8e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "external_id": "12345678",
  "access_level": 10,
  "expires_at": "2027-09-26T00:00:00Z",
  "created_at": "2026-09-26T09:14:07.412903Z",
  "last_checked_at": "2026-09-26T09:19:02.113045Z",
  "last_used_ips": [],
  "needs_attention": false,
  "secret": "glpat-Xy7Q2kN4mP8rT1vW6yB3",
  "secret_notice": "This is the only time the secret is returned. Feint does not store it."
}

Alerts#

What the monitor has recorded about them.

The event object#

Fields

id
uuid
The event's id. De-duplicate on it.
canary_token_id
uuid
The canary the event concerns.
kind
string
canary.used, canary.revoked_externally, canary.missing or canary.expired.
occurred_at
datetime
The provider's own timestamp for the use, where it has one. GitLab reports use at most once per ten minutes or so, so this marks a window, not a single call. For an AWS decoy, the time of the first CloudTrail record in the event's window: later calls within about fifteen minutes join the same event and are counted in detail.
detected_at
datetime
When Feint's monitor saw it.
source_ips
array of string
Addresses the provider reported for the use. May be empty.
detail
object
Context for the notification. For canary.used: the placement, provider and external id; for AWS decoys also the telemetry CloudTrail gave us for the window: count (exact), calls (one entry per call, in order, up to the first 50: occurred_at, action as service:Operation, error with AWS's error code or null when the call succeeded, source_ip, user_agent), first_seen_at, last_seen_at, records_truncated and source (cloudtrail). Every call is at records_url.
created_at
datetime
When the event was written.
records_url
string
Where to fetch the provider's records behind this event (GET /v1/alerts/{id}/records). Empty for providers that have none.
client
object | null
MSSP partners: the client whose canary this is. Null for your own organization's.
The event object
{
  "id": "0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f",
  "canary_token_id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "kind": "canary.used",
  "occurred_at": "2026-09-26T09:41:00Z",
  "detected_at": "2026-09-26T09:45:12.088311Z",
  "source_ips": ["203.0.113.44"],
  "detail": {
    "external_id": "12345678",
    "placement": "prod .env on web-01",
    "provider": "gitlab"
  },
  "created_at": "2026-09-26T09:45:12.088311Z",
  "records_url": "https://api.feintsecurity.com/v1/alerts/0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f/records"
}

The records object#

Fields

event_id
uuid
The event the records belong to.
canary_token_id
uuid
The canary the event concerns.
count
integer
How many records are stored for the event.
truncated
boolean
True when the window held more calls than Feint stores per event (500); the event's detail.count is still exact.
records
array of object
The records, oldest first.
The records object
{
  "event_id": "0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f",
  "canary_token_id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "count": 1,
  "truncated": false,
  "records": [
    {
      "occurred_at": "2026-10-03T14:19:13Z",
      "provider_event_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
      "record": {
        "awsRegion": "us-east-1",
        "eventID": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
        "eventName": "GetCallerIdentity",
        "eventSource": "sts.amazonaws.com",
        "eventTime": "2026-10-03T14:19:13Z",
        "eventType": "AwsApiCall",
        "readOnly": true,
        "sourceIPAddress": "203.0.113.9",
        "tlsDetails": {
          "cipherSuite": "TLS_AES_128_GCM_SHA256",
          "clientProvidedHostHeader": "sts.us-east-1.amazonaws.com",
          "tlsVersion": "TLSv1.3"
        },
        "userAgent": "aws-cli/2.37.7 md/awscrt#0.27.4",
        "userIdentity": {"accessKeyId": "AKIAEXAMPLEKEY000001"}
      }
    }
  ]
}

GET/v1/alerts#

List canary events

Requires alerts:read

Events the monitor has recorded for the organization's canaries, newest detected_at first. limit is clamped to 1–500. There is no cursor yet: poll, and de-duplicate on id.

Query parameters

limit
integer · default 100
How many events to return, newest first. Values outside 1–500 are clamped.
X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns an array of events.

Errors

401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
422The request could not be read as this endpoint expects.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl "https://api.feintsecurity.com/v1/alerts?limit=100" \
  -H "Authorization: Bearer $FEINT_API_KEY"
r = requests.get(f"{API}/alerts", headers=HEADERS, params={"limit": 100})
r.raise_for_status()
events = r.json()
200 OK
[
  {
    "id": "0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f",
    "canary_token_id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
    "kind": "canary.used",
    "occurred_at": "2026-09-26T09:41:00Z",
    "detected_at": "2026-09-26T09:45:12.088311Z",
    "source_ips": ["203.0.113.44"],
    "detail": {
      "external_id": "12345678",
      "placement": "prod .env on web-01",
      "provider": "gitlab"
    },
    "created_at": "2026-09-26T09:45:12.088311Z",
    "records_url": "https://api.feintsecurity.com/v1/alerts/0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f/records"
  }
]

GET/v1/alerts/{event_id}/records#

Get an event's records

Requires alerts:read

The provider's own records of what was done with the decoy, for incident response. For AWS decoys, every CloudTrail record in the event's window: who called from where, with what tool, which action, its parameters and its result, in CloudTrail's own field names. Each record is reduced to the actor's request and AWS's answer; the rest of what CloudTrail wrote describes Feint's side of the trail and is not served. GitLab events have none, and answer with an empty list. An event outside your organization is a 404.

Path parameters

event_idrequired
string
The event's id, from GET /v1/alerts.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns a records.

Errors

401No valid API key.
403The key lacks the scope this endpoint needs.
404No canary with that id here, or `X-Feint-Client` names no client of yours.
422The request could not be read as this endpoint expects.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl https://api.feintsecurity.com/v1/alerts/$EVENT_ID/records \
  -H "Authorization: Bearer $FEINT_API_KEY"
event_id = "…"

r = requests.get(f"{API}/alerts/{event_id}/records", headers=HEADERS)
r.raise_for_status()
records = r.json()
200 OK
{
  "event_id": "0b6d0f3e-7c1a-4e2b-8f9d-1a2b3c4d5e6f",
  "canary_token_id": "8d3e6f1a-2b4c-4d5e-9f01-23456789abcd",
  "count": 1,
  "truncated": false,
  "records": [
    {
      "occurred_at": "2026-10-03T14:19:13Z",
      "provider_event_id": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
      "record": {
        "awsRegion": "us-east-1",
        "eventID": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
        "eventName": "GetCallerIdentity",
        "eventSource": "sts.amazonaws.com",
        "eventTime": "2026-10-03T14:19:13Z",
        "eventType": "AwsApiCall",
        "readOnly": true,
        "sourceIPAddress": "203.0.113.9",
        "tlsDetails": {
          "cipherSuite": "TLS_AES_128_GCM_SHA256",
          "clientProvidedHostHeader": "sts.us-east-1.amazonaws.com",
          "tlsVersion": "TLSv1.3"
        },
        "userAgent": "aws-cli/2.37.7 md/awscrt#0.27.4",
        "userIdentity": {"accessKeyId": "AKIAEXAMPLEKEY000001"}
      }
    }
  ]
}

Clients#

MSSP partners: the clients a key can act as, via `X-Feint-Client`.

The client object#

Fields

id
string
The client's id. Send it as X-Feint-Client to act as this client.
name
string
The client's name, as set in the console.
The client object
{
  "id": "cli_4f1c9a2b7e3d4c5a8b6e0f1d2c3b4a59",
  "name": "Acme Industrial"
}

GET/v1/clients#

List your clients

Requires tokens:read

For MSSP partners: the clients your key can act as. Send a client's id as the X-Feint-Client header on any other endpoint to act as that client; without it, requests act as your own organization. Archived clients are not listed and cannot be acted as.

Query parameters

X-Feint-Client
string | null
MSSP partners: act as this client (an id from GET /v1/clients). Omit to act as your own organization.

Returns 200

Returns an array of clients.

Errors

401No valid API key.
403The key lacks the scope this endpoint needs.
429Too many requests with this key, or from this address. `Retry-After` says how long to wait.
curl https://api.feintsecurity.com/v1/clients \
  -H "Authorization: Bearer $FEINT_API_KEY"
r = requests.get(f"{API}/clients", headers=HEADERS)
r.raise_for_status()
clients = r.json()
200 OK
[
  {
    "id": "cli_4f1c9a2b7e3d4c5a8b6e0f1d2c3b4a59",
    "name": "Acme Industrial"
  }
]