---
name: functions-falcon-api
description: Call CrowdStrike Falcon platform APIs (detections, alerts, hosts, RTR) from within Foundry function handlers. TRIGGER when user asks to "call Falcon APIs from a function", "use FalconPy in a function", "use gofalcon in a function", or needs to integrate Falcon platform APIs within serverless function code. DO NOT TRIGGER when user wants to expose external third-party APIs to Foundry — use api-integrations instead.
version: 1.4.1
updated: 2026-08-07
tags: [foundry, functions, falcon-api, falconpy, gofalcon]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: backend
---

# Falcon API Integration in Functions

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Falcon API integration specialist for Foundry functions**.
>
> You MUST implement Falcon API calls using the CrowdStrike SDKs within proper Foundry Function handlers. Authentication is automatic when using the FDK handler pattern.
>
> The FalconPy `Detects` class is **removed**. Do not import it. Use `Alerts` for detection queries.

This skill covers calling CrowdStrike Falcon APIs from within Foundry functions (serverless Go or Python code). Authentication is completely automatic when code runs inside Foundry function handlers — the platform handles all OAuth flows, token management, and credential injection.

For exposing external APIs to Foundry via OpenAPI specs, see **api-integrations** instead.

> **🚫 DEPRECATED API — NEVER USE:**
>
> **Do NOT import or use the `Detects` class from FalconPy.** The Detects API (`/detects/entities/detects/v2`) is deprecated and returns **405 Method Not Allowed**. Any code using `Detects()`, `query_detects()`, or `get_detect_summaries()` will fail at runtime.
>
> **Use instead:** `from falconpy import Alerts` with `query_alerts_v2()` / `get_alerts_v2()`. Filter by `product:'detections'` to scope to detections only.

## Reference Files

| Topic | Reference |
|-------|-----------|
| Retry decorator with exponential backoff, counter-rationalizations table | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Python: Zero-Argument Authentication

FalconPy Service Classes require zero arguments when called inside Foundry Function handlers. The `crowdstrike.foundry.function` FDK provides the handler decorator that enables automatic authentication:

```python
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import Alerts, Hosts

func = Function.instance()

@func.handler(method='GET', path='/api/alerts')
def get_alerts(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    falcon = Alerts()  # Zero-arg constructor — auth is automatic

    limit = min(int(request.params.get("limit", 50)), 100)
    # FQL filter: high-severity alerts from the last 24 hours.
    # Combine conditions with '+' (AND); relative times like 'now-24h' are supported.
    response = falcon.query_alerts_v2(
        filter="severity_name:'High'+created_timestamp:>'now-24h'",
        limit=limit,
        sort="created_timestamp|desc",
    )

    if response["status_code"] != 200:
        logger.error(f"Failed to query alerts: {response.get('errors')}")
        return Response(body={"error": "Failed to fetch alerts"}, code=500)

    alert_ids = response.get("body", {}).get("resources", [])
    if not alert_ids:
        return Response(body={"alerts": []}, code=200)

    details_response = falcon.get_alerts_v2(ids=alert_ids)
    if details_response["status_code"] != 200:
        return Response(body={"error": "Failed to fetch alert details"}, code=500)

    alerts = details_response.get("body", {}).get("resources", [])
    return Response(body={"alerts": alerts}, code=200)

if __name__ == '__main__':
    func.run()
```

**How it works:**
- **In Foundry cloud**: Uses context-based authentication injected by the platform
- **Locally**: Reads `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` from environment variables

FalconPy already reads env vars internally, so writing a `get_falcon_client()` wrapper adds no value and breaks context auth in the cloud.

## Go: FDK Helper Authentication

Go requires the FDK helper to get cloud and user-agent configuration:

```go
package main

import (
    "context"
    "log/slog"
    "github.com/crowdstrike/gofalcon/falcon"
    "github.com/crowdstrike/gofalcon/falcon/client"
    fdk "github.com/crowdstrike/foundry-fn-go"
)

func newHandler(_ context.Context, _ *slog.Logger, _ fdk.SkipCfg) fdk.Handler {
    m := fdk.NewMux()

    m.Get("/api/alerts", fdk.HandleFnOf(func(ctx context.Context, r fdk.RequestOf[struct{}]) fdk.Response {
        accessToken := r.Header.Get("X-CS-ACCESSTOKEN")

        opts := fdk.FalconClientOpts()
        falconClient, err := falcon.NewClient(&falcon.ApiConfig{
            AccessToken:       accessToken,
            Cloud:             falcon.Cloud(opts.Cloud),
            Context:           ctx,
            UserAgentOverride: opts.UserAgent,
        })
        if err != nil {
            return fdk.Response{Code: 500, Body: fdk.JSON(map[string]string{"error": "Failed to authenticate"})}
        }

        // ... API calls with falconClient ...
        return fdk.Response{Code: 200, Body: fdk.JSON(map[string]interface{}{"alerts": []interface{}{}})}
    }))

    return m
}

func main() {
    fdk.Run(context.Background(), newHandler)
}
```

## Common API Patterns

### Detection Queries (via Alerts API)

> **⚠️ The legacy Detects API (`/detects/entities/detects/v2`) is deprecated and returns 405 Method Not Allowed.** Use the Alerts API (`/alerts/entities/alerts/v3`) for all detection queries — it covers both detections and cases.

```python
@func.handler(method='GET', path='/api/detections')
def get_detections(request: Request, config, logger) -> Response:
    falcon = Alerts()  # Zero-arg — auth is automatic

    severity_min = int(request.params.get("severity_min", 3))
    limit = min(int(request.params.get("limit", 50)), 100)

    # Use Alerts v2 methods — these hit /alerts/entities/alerts/v3 under the hood.
    # FQL filter: severity threshold + product "detections" (excludes cases/incidents).
    query_response = falcon.query_alerts_v2(
        filter=f"severity:>='{severity_min}'+product:'detections'",
        limit=limit,
        sort="created_timestamp|desc",
    )
    if query_response["status_code"] != 200:
        return Response(body={"error": "Failed to query detections"}, code=500)

    alert_ids = query_response.get("body", {}).get("resources", [])
    if not alert_ids:
        return Response(body={"detections": []}, code=200)

    details = falcon.get_alerts_v2(ids=alert_ids)
    if details["status_code"] != 200:
        return Response(body={"error": "Failed to get details"}, code=500)

    return Response(body={"detections": details["body"]["resources"]}, code=200)
```

### Host Lookups

```python
@func.handler(method='GET', path='/api/hosts/{hostname}')
def get_host_details(request: Request, config, logger) -> Response:
    falcon = Hosts()

    hostname = request.params.get("hostname")
    if not hostname:
        return Response(body={"error": "Hostname required"}, code=400)

    query = falcon.query_devices_by_filter(filter=f"hostname:'{hostname}'")
    if query["status_code"] != 200:
        return Response(body={"error": "Failed to query devices"}, code=500)

    host_ids = query.get("body", {}).get("resources", [])
    if not host_ids:
        return Response(body={"error": f"Host not found: {hostname}"}, code=404)

    details = falcon.get_device_details(ids=host_ids)
    host = details.get("body", {}).get("resources", [{}])[0]
    return Response(body={"host": host}, code=200)
```

### Multi-API Enrichment

```python
@func.handler(method='POST', path='/api/enrich')
def enrich_host_context(request: Request, config, logger) -> Response:
    hosts_api = Hosts()
    alerts_api = Alerts()

    hostname = request.body.get("hostname")
    if not hostname:
        return Response(body={"error": "Hostname required"}, code=400)

    # Get host
    host_query = hosts_api.query_devices_by_filter(filter=f"hostname:'{hostname}'")
    host_ids = host_query.get("body", {}).get("resources", [])
    if not host_ids:
        return Response(body={"error": "Host not found"}, code=404)

    host = hosts_api.get_device_details(ids=host_ids).get("body", {}).get("resources", [{}])[0]

    # Get detections (via Alerts API with product filter)
    detection_ids = alerts_api.query_alerts_v2(filter=f"device.hostname:'{hostname}'+product:'detections'", limit=10).get("body", {}).get("resources", [])
    detections = alerts_api.get_alerts_v2(ids=detection_ids).get("body", {}).get("resources", []) if detection_ids else []

    # Get all alerts (includes detections + cases)
    alert_ids = alerts_api.query_alerts_v2(filter=f"device.hostname:'{hostname}'", limit=10).get("body", {}).get("resources", [])
    alerts = alerts_api.get_alerts_v2(ids=alert_ids).get("body", {}).get("resources", []) if alert_ids else []

    return Response(body={"host": host, "detections": detections, "alerts": alerts}, code=200)
```

## LogScale / NG-SIEM Queries from Functions

When you need to **query** LogScale data (e.g., workflow execution stats from the "fusion" repo, detection telemetry, custom ingested events), use the `NGSIEM` class. This is distinct from **ingestion** (covered in functions-development).

> **⚠️ Class Disambiguation:**
> - `NGSIEM` — Use for **querying** (async search jobs) and file uploads (lookup files, CSV imports). This is the query interface.
> - `FoundryLogScale` — Use only for **ingestion** (`ingest_data`). Despite the name suggesting broad LogScale functionality, it does NOT have the search methods.

### Query Pattern (Python)

```python
import time
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import NGSIEM

func = Function.instance()

# Always "search-all" — see the gotcha below. Scope to a repo in the query, not here.
REPO = "search-all"


def run_logscale_query(ngsiem, query_string, start, end, logger, max_wait=40):
    """Execute a CQL query as an async search job and return result rows.

    ``start`` / ``end`` accept Humio relative strings ("24h", "7d", "30d",
    "now") or epoch-millisecond integers.
    """
    payload = {"queryString": query_string, "start": start, "end": end, "isLive": False}
    # Must be search=, not body= — see the keyword gotcha below.
    started = ngsiem.start_search(repository=REPO, search=payload)

    if not isinstance(started, dict) or started.get("status_code", 500) >= 300:
        logger.error(f"start_search failed: {started}")
        return None

    # start_search returns "resources"; get_search_status returns "body".
    job_id = (started.get("resources") or {}).get("id")
    if not job_id:
        logger.error(f"start_search returned no job id: {started}")
        return None

    # Poll until done
    waited = 0.0
    poll_interval = 1.5
    while waited < max_wait:
        time.sleep(poll_interval)
        waited += poll_interval
        status = ngsiem.get_search_status(repository=REPO, id=job_id)
        if not isinstance(status, dict) or status.get("status_code", 500) >= 300:
            logger.error(f"get_search_status failed: {status}")
            return None
        sbody = status.get("body") or {}
        if sbody.get("done"):
            return sbody.get("events", []) or []

    logger.warning(f"query timed out after {max_wait}s: {query_string}")
    return None


@func.handler(method='POST', path='/api/query')
def handle_query(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    ngsiem = NGSIEM()  # Zero-arg auth — automatic in Foundry

    query = request.body.get("query", "")
    start = request.body.get("start", "24h")
    end = request.body.get("end", "now")

    events = run_logscale_query(ngsiem, query, start, end, logger)
    if events is None:
        return Response(body={"error": "LogScale query failed"}, code=500)

    return Response(body={"results": events, "count": len(events)}, code=200)

if __name__ == '__main__':
    func.run()
```

### The `search=` Keyword Gotcha

**CRITICAL:** Pass the query payload as `search=`, not `body=`. FalconPy's guard reads only `kwargs.get("search")`, so `body=` returns a local error without issuing a request. The docstring lists `body` as accepted, but the guard ignores it ([falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491)).

Response keys are asymmetric: `start_search` renames its payload to `resources` (read `started["resources"]["id"]`), while `get_search_status` does not (read `status["body"]`).

### The "search-all" Repository Gotcha

**CRITICAL:** Always pass `repository="search-all"` when querying from Foundry functions. Passing a specific repository name (e.g., `"fusion"`, `"main"`) causes **403 Forbidden** errors at runtime (`"scope not permitted"`), even if the repository exists and the app has `humio-auth-proxy` scopes granted.

The NG-SIEM queryjobs API is addressed by **searchable view**, not by raw repo name. Use `search-all` as the repository and add `#repo=fusion` (or whichever repo you need) as a filter prefix in your query string:

```python
# Filter to a specific repo within the query itself
query = "#repo=fusion | execution_log_type=summary | execution_log_subtype=end | groupby([status], function=count(execution_id, distinct=true))"
events = run_logscale_query(ngsiem, query, "24h", "now", logger)
```

### Required OAuth Scopes

```yaml
# manifest.yml
auth:
    scopes:
        - humio-auth-proxy:read     # Required for queries
        - humio-auth-proxy:write    # Required only if also uploading lookup files
```

### Reference

- [Exporting Falcon Next-Gen SIEM Query Results to CSV with Falcon Foundry](https://www.crowdstrike.com/tech-hub/ng-siem/exporting-falcon-next-gen-siem-query-results-to-csv-with-falcon-foundry/) — background on async LogScale querying from Foundry, plus CSV export. Note that this post reaches for `FoundryLogScale` with `mode="async"`; the `NGSIEM` pattern above is what has been verified end-to-end against the queryjobs API in a deployed app. Use the pattern above, and treat the post as context for the surrounding workflow (time ranges, result handling, export).

## The 207 Multi-Status Gotcha

CrowdStrike APIs may return `207 Multi-Status` responses that look successful but contain embedded errors. Check the errors array:

```python
response = falcon.perform_action(action_name="contain", ids=host_ids)

if response["status_code"] == 207:
    errors = response.get("body", {}).get("errors", [])
    rate_limited = [e for e in errors if e.get("code") == 429]
    if rate_limited:
        return Response(body={"error": "Rate limited", "failed_ids": [e.get("id") for e in rate_limited]}, code=429)
```

## Testing

Mock Falcon APIs in tests instead of making real API calls (they are slow, flaky, and quota-consuming):

```python
def test_get_alerts_success():
    mock_falcon = Mock()
    mock_falcon.query_alerts_v2.return_value = {
        "status_code": 200,
        "body": {"resources": ["alert-001", "alert-002"]}
    }
    mock_falcon.get_alerts_v2.return_value = {
        "status_code": 200,
        "body": {"resources": [{"id": "alert-001", "severity": 80}]}
    }

    with patch('falconpy.Alerts', return_value=mock_falcon):
        from main import get_alerts
        request = Mock(spec=Request)
        request.params = {"limit": "10"}
        response = get_alerts(request, None, Mock())
        assert response.code == 200
        assert len(response.body["alerts"]) == 1
```

## Local Testing

```bash
export FALCON_CLIENT_ID="your-client-id"
export FALCON_CLIENT_SECRET="your-client-secret"
cd functions/my-function && python3 main.py
curl -X GET http://localhost:8081/api/alerts?limit=10
```

## OAuth Scopes for manifest.yml

Every FalconPy service class call requires the correct OAuth scope(s) declared in your manifest's `auth.scopes` array. Without the right scopes, the function gets a 403 at runtime. `foundry apps validate` does NOT catch missing scopes — it only fails at runtime.

> **⚠️ Scope names don't always match class names.** The `Hosts` class requires `devices:read`, not `hosts:read`. Always use this table rather than guessing from class names.

> **Built-in capabilities don't need scopes.** API integrations, collections, workflows, and LogScale ingestion work without declaring their scopes when used through Foundry's built-in SDK patterns (`falcon.apiIntegration()`, `CustomStorage()` for app collections, etc.). Only declare scopes when calling Falcon platform APIs directly via FalconPy service classes.

### Scope Reference (verified from production sample apps)

Each row maps a FalconPy method actually called in a sample function to the scope declared in that app's manifest.

| FalconPy Class | Methods | Required Scope(s) | Verified In |
|---|---|---|---|
| `Hosts` | `get_device_details` | `devices:read` | foundry-sample-functions-python |
| `Intel` | `query_indicator_ids` | `falconx-indicators:read` | foundry-sample-zscaler-internet-access |
| `IdentityProtection` | `graphql`, `query_sensors`, `get_sensor_details` | `identity-graphql:write`, `identity-entities:read` | foundry-sample-idp-notifications |
| `IdentityProtection` | `query_policy_rules`, `get_policy_rules`, `delete_policy_rules` | `identity-policy-rules:read`, `identity-policy-rules:write` | foundry-sample-servicenow-idp |
| `NGSIEM` | `upload_file` | `humio-auth-proxy:write` | foundry-sample-ngsiem-importer |
| `NGSIEM` | `start_search`, `get_search_status` | `humio-auth-proxy:read` | Verified against a live CID (200 + results); see LogScale Queries section |
| `FoundryLogScale` | `ingest_data` | `app-logs:read`, `app-logs:write` | foundry-sample-logscale |
| `FirewallManagement` | `create_rule_group`, `query_events`, `get_events` | `firewall-management:read`, `firewall-management:write` | foundry-sample-category-blocking |
| `HostGroup` | `query_host_groups`, `get_host_groups` | `host-group:read`, `host-group:write` | foundry-sample-category-blocking |

**Go functions (gofalcon) require the same scopes.** The table above uses FalconPy class/method names, but the underlying Falcon API scopes are identical regardless of SDK. If your Go function calls the RTR admin API, declare `real-time-response-admin:write`. If it manages incidents, declare `incidents:read`, `incidents:write`.

### How to declare scopes

```yaml
# manifest.yml
auth:
    scopes:
        - devices:read
        - falconx-indicators:read
    permissions: {}
    roles: []
```

### When unsure about the correct scope

If you're using a FalconPy method not in this table:
1. Check the method's HTTP verb and API path in FalconPy source — GET typically needs `:read`, POST/PUT/PATCH/DELETE typically needs `:write`
2. The scope prefix is usually the **API path prefix** (e.g., `/iocs/...` → `iocs`, `/devices/...` → `devices`), but exceptions exist (`Hosts` → `devices`, `NGSIEM` → `humio-auth-proxy`)
3. When ambiguous, **ask the user** which scopes to include rather than guessing

## Falcon Severity Values

CrowdStrike APIs return severity as **integers** (1-5) or display names. When integrating with external systems (Jira, ServiceNow, email), map them explicitly:

| Falcon Severity | Display Name | Typical External Mapping |
|----------------|--------------|--------------------------|
| 1 | Informational | Low / Lowest |
| 2 | Low | Low |
| 3 | Medium | Medium |
| 4 | High | High |
| 5 | Critical | Highest / Critical |

Use `max_severity_displayname` for FQL filters (string comparison) or `max_severity` for numeric comparison. When passing severity to external ticketing systems, always map to their expected format rather than passing the raw value through.

## Common Pitfalls

- **Writing OAuth code or credential management.** Auth is automatic inside FDK handlers. The zero-arg pattern (`Hosts()`, `Alerts()`) handles all auth. (Go requires `fdk.FalconClientOpts()` -- see above.)
- **Using `requests` library instead of CrowdStrike SDKs.** SDKs handle auth, retries, pagination, and region discovery.
- **Passing credentials explicitly to constructors.** Use zero-arg constructors (`Alerts()`, `Hosts()`). Do NOT write `IOC(client_id=os.environ["FALCON_CLIENT_ID"], client_secret=...)` -- this breaks context-based auth in the Foundry cloud.
- **Writing Falcon API calls outside of FDK handler functions.** The handler pattern is required for automatic auth injection.
- **Not handling 207 Multi-Status.** These responses look successful but may contain embedded errors.

## Use Cases

For real-world implementation patterns, see:
- [python-functions.md](../../use-cases/python-functions.md) — Python handler patterns, SDK usage, testing

## Reference Implementations

- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: Reference Python patterns. See also [Dive into Falcon Foundry Functions with Python](https://www.crowdstrike.com/tech-hub/ng-siem/dive-into-falcon-foundry-functions-with-python/).
- **[foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream)**: Side-by-side Go and Python auth patterns.
- **[foundry-sample-detection-translation](https://github.com/CrowdStrike/foundry-sample-detection-translation)**: CrowdStrike alerts API from functions.
- **[foundry-sample-threat-intel](https://github.com/CrowdStrike/foundry-sample-threat-intel)**: CrowdStrike Intelligence APIs from functions.
- **[foundry-sample-idp-notifications](https://github.com/CrowdStrike/foundry-sample-idp-notifications)**: Falcon IdP domain and connector monitoring.
