API version: v1 · Base URL: https://froddy.io · Last updated: February 2026
All endpoints except /health and the demo tenant require the X-API-Key header.
X-API-Key: your_tenant_api_key
Each tenant receives a unique API key during provisioning. The key identifies both the caller and the tenant scope — all data returned is isolated to that tenant.
Demo tenant: The demo tenant is publicly accessible without an API key. It is intended for evaluation and interactive demos at froddy.io/demo. Do not use it for production data.
Admin endpoints (tenant management) require a separate admin key passed via the same X-API-Key header.
X-API-Key header exclusively. Authorization: Bearer is not supported. Missing or invalid keys return HTTP 401 with body {"detail": "Invalid or missing API key"}. Admin endpoints return HTTP 501 if the admin key is not configured on the server.The core endpoint. Call this before each payout to receive a verdict.
[TENANT] POST /v1/evaluate
curl -X POST https://froddy.io/v1/evaluate \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{
"event_id": "payout_12345",
"entity_id": "partner_42",
"amount": 15000.00,
"currency": "USD",
"event_type": "payout",
"device_hash": "abc123def456"
}'
| Field | Type | Required | Description |
|---|---|---|---|
event_id |
string | yes | Unique transaction identifier. Used for idempotency (see Idempotency). |
entity_id |
string | yes | Identifier of the counterparty (partner, publisher, seller). Opaque to RCL. |
amount |
number | yes | Transaction amount in the specified currency. |
currency |
string | yes | Currency code (e.g. "USD"). |
event_type |
string | yes | Operation category (e.g. "payout"). |
device_hash |
string | no | Opaque source identifier used for deduplication grouping. Not a device fingerprint. If omitted, the R-DEDUP rule is skipped. |
currency is a freeform string (max 3 characters, defaults to "USD"). event_type is a freeform string (defaults to "payout"); it is metadata only and does not affect rule evaluation. amount must be >= 0 (enforced via Pydantic ge=0); there is no upper bound. All amounts are treated as USD-equivalent for threshold comparison regardless of the currency field.{
"event_id": "payout_12345",
"verdict": "allow",
"rule_id": null,
"reason": "All rules passed",
"evaluated_at": "2026-02-20T14:30:00"
}
{
"event_id": "payout_12345",
"verdict": "hold-for-review",
"rule_id": "R-CEIL",
"reason": "daily ceiling exceeded: $53,000 / $50,000",
"evaluated_at": "2026-02-20T14:30:00"
}
{
"event_id": "payout_12345",
"verdict": "block",
"rule_id": "R-COHORT",
"reason": "single transaction $150,000 >= block threshold $100,000",
"evaluated_at": "2026-02-20T14:30:00"
}
| Field | Type | Description |
|---|---|---|
event_id |
string | Echo of the submitted event_id. Serves as the decision identifier. |
verdict |
string | One of: "allow", "hold-for-review", "block" |
rule_id |
string or null | The rule that triggered the verdict. null if verdict is allow. One of: "R-COHORT", "R-CEIL", "R-VEL", "R-DEDUP" |
reason |
string | Human-readable explanation. "All rules passed" for allow verdicts. |
evaluated_at |
string (ISO 8601) | Timestamp of evaluation. |
Note: The response does not include decision_id, policy_version, or shadow fields. The event_id serves as the decision identifier. Shadow mode status can be checked via the health endpoint.
| Verdict | Meaning | Recommended Client Action |
|---|---|---|
allow |
Transaction within normal parameters | Proceed with payout |
hold-for-review |
Anomaly detected, review recommended | Queue for manual review |
block |
Clear anomaly, high confidence | Reject or escalate |
Rules are evaluated in fixed priority: R-COHORT → R-CEIL → R-VEL → R-DEDUP. The first rule to trigger determines the verdict. If no rule triggers, the verdict is allow.
For rule parameters and default thresholds, see Policy Management and the Rules Reference in the product docs.
Query the log of all evaluation decisions for this tenant.
[TENANT] GET /v1/decisions
Returns a list of past evaluation decisions with filtering support.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_id |
string | Filter by entity |
verdict |
string | Filter by verdict: allow, hold-for-review, block |
scenario |
string | Filter by scenario / event type |
limit (integer, default 100, max 1000), entity_id (string), verdict (string), tenant (string), scenario (string). Pagination is limit-based (no cursor, no offset). Date range filtering is not supported on this endpoint — use /v1/decisions/export for date filtering. Response: {"decisions": [...], "count": N} where each decision object contains: id, event_id, entity_id, amount, currency, event_type, event_ts, tenant, scenario, verdict, rule_id, rule_snapshot (JSON string), reason, evaluated_at, device_hash.Example:
curl -X GET "https://froddy.io/v1/decisions?verdict=block&entity_id=partner_42" \
-H "X-API-Key: your_key"
Example response:
{
"decisions": [
{
"id": 42,
"event_id": "payout_12345",
"entity_id": "partner_42",
"amount": 15000.0,
"currency": "USD",
"event_type": "payout",
"event_ts": "2026-02-20T14:30:00",
"tenant": "demo",
"scenario": "default",
"verdict": "hold-for-review",
"rule_id": "R-CEIL",
"rule_snapshot": "{\"daily_total\": 53000, \"ceiling_usd\": 50000}",
"reason": "daily ceiling exceeded: $53,000 / $50,000",
"evaluated_at": "2026-02-20T14:30:01",
"device_hash": null
}
],
"count": 1
}
[TENANT] GET /v1/decisions/export
Exports decisions in JSON format with date filters. Supports up to 50,000 records per request.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
from |
string | Start date for export range |
to |
string | End date for export range |
date_from, date_to) use ISO 8601 format (e.g. 2026-02-01). Additional filter: verdict (string) is supported. entity_id filtering is not available on export. Format parameter: format=json (default) returns {"tenant": "...", "count": N, "decisions": [...]}; format=csv returns a file download with Content-Disposition header. The response is not streamed — all rows are loaded in memory. The 50K limit is a silent truncation (rows beyond 50,000 are not returned; no error is raised).Policy is a JSON object containing all rule thresholds for a tenant. It is versioned automatically — every update increments the version number and stores a snapshot.
[TENANT] GET /v1/policy
Returns the active policy, its version number, and the date of last update.
curl -X GET https://froddy.io/v1/policy \
-H "X-API-Key: your_key"
Response:
{
"version": 5,
"updated_at": "2026-02-20T14:30:00Z",
"policy": {
"R-COHORT": {
"hold_usd": 25000,
"block_usd": 100000
},
"R-CEIL": {
"daily_ceiling_usd": 50000,
"block_multiplier": 1.5
},
"R-VEL": {
"window_hours": 1,
"max_count": 20,
"block_multiplier": 2
},
"R-DEDUP": {
"max_entities": 3,
"block_entities": 6,
"window_hours": 24
},
"entity_overrides": {
"partner_vip": {
"R-CEIL": { "daily_ceiling_usd": 200000 },
"R-COHORT": { "hold_usd": 50000, "block_usd": 300000 }
}
}
}
}
{"version": 3, "policy": {...}, "updated_at": "2026-02-20T14:30:00"}. The version is an integer that auto-increments on each update. updated_at is ISO 8601 format.| Rule | Parameter | Type | Default | Description |
|---|---|---|---|---|
| R-COHORT | hold_usd |
number | 25,000 | Single transaction ≥ this amount → HOLD |
| R-COHORT | block_usd |
number | 100,000 | Single transaction ≥ this amount → BLOCK |
| R-CEIL | daily_ceiling_usd |
number | 50,000 | Rolling 24h cumulative limit per entity |
| R-CEIL | block_multiplier |
number | 1.5 | Ceiling × multiplier exceeded → BLOCK (otherwise HOLD) |
| R-VEL | max_count |
number | 20 | Max transactions in window → HOLD |
| R-VEL | window_hours |
number | 1 | Time window in hours |
| R-VEL | block_multiplier |
number | 2 | max_count × multiplier exceeded → BLOCK |
| R-DEDUP | max_entities |
number | 3 | ≥ N entities sharing the same source hash → HOLD |
| R-DEDUP | block_entities |
number | 6 | ≥ N entities sharing the same source hash → BLOCK |
| R-DEDUP | window_hours |
number | 24 | Observation window in hours |
The entity_overrides object is optional. Keys are entity IDs; values are partial policy objects that override global thresholds for that specific entity.
[TENANT] PUT /v1/policy
Replaces the current policy. The version number auto-increments. A snapshot of the previous policy is stored in the history.
curl -X PUT https://froddy.io/v1/policy \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{
"R-COHORT": { "hold_usd": 30000, "block_usd": 120000 },
"R-CEIL": { "daily_ceiling_usd": 75000, "block_multiplier": 1.5 },
"R-VEL": { "window_hours": 1, "max_count": 25, "block_multiplier": 2 },
"R-DEDUP": { "max_entities": 3, "block_entities": 6, "window_hours": 24 }
}'
400. Response: {"version": N, "policy": {...}, "updated_at": "..."} with the new version number. There is no server-side validation of rule parameter names or values within the policy — any JSON object is accepted. Invalid rule keys are only validated on the entity-overrides endpoint.[TENANT] GET /v1/policy/history
Returns all previous policy versions with their snapshots.
curl -X GET https://froddy.io/v1/policy/history \
-H "X-API-Key: your_key"
{"tenant": "...", "history": [...], "count": N}. Each history entry: {"version": 3, "policy": {...}, "changed_at": "2026-02-20T14:30:00"}. Ordered by version descending (newest first). Limited to the last 20 versions. No pagination — the limit is fixed server-side.[TENANT] POST /v1/policy/rollback/{version}
Reverts the active policy to a specific historical version. This creates a new version (the version counter continues incrementing — it does not reset).
curl -X POST https://froddy.io/v1/policy/rollback/3 \
-H "X-API-Key: your_key"
{"tenant": "...", "rolled_back_to": 3, "new_version": 5, "policy": {...}, "updated_at": "..."}. Rollback creates a new version (increments the counter) with the content of the target version — it does not rewind the version number. If the specified version does not exist in history, returns HTTP 404 with {"detail": "Version N not found in history for tenant '...'"}.[TENANT] GET /v1/rules
Returns the set of active rules and their current parameters, derived from the active policy. If the policy changes, this endpoint reflects the new state immediately.
curl -X GET https://froddy.io/v1/rules \
-H "X-API-Key: your_key"
{"rules": [...]}. Each rule object: {"rule_id": "R-CEIL", "name": "Daily exposure ceiling", "description": "...", "type": "ceiling", "params": {...}}. Rules R-COHORT, R-CEIL, and R-VEL are always included. R-DEDUP is only included if configured in the policy (i.e., if the policy contains an R-DEDUP or R-DEVICE key with non-empty config). There is no concept of "disabled" rules — a rule is either present in the policy or absent.Aggregated statistics for the current tenant.
[TENANT] GET /v1/stats
Returns aggregated counters: total evaluations, count by verdict, total amounts.
curl -X GET https://froddy.io/v1/stats \
-H "X-API-Key: your_key"
{"total": 150, "allow_count": 120, "hold_count": 20, "block_count": 10, "blocked_amount": 45000.0, "held_amount": 32000.0}. Stats cover all-time (no window, no date range parameters). Filter parameters: tenant (string), scenario (string).[TENANT] GET /v1/stats/timeseries
Returns verdict counts over time (for charting purposes). Supports hourly and daily buckets.
curl -X GET "https://froddy.io/v1/stats/timeseries" \
-H "X-API-Key: your_key"
bucket ("hour" or "day", default "hour"), tenant (string), scenario (string). No date range filter — returns the last 168 buckets (7 days hourly, or ~24 weeks daily). Response: {"bucket": "hour", "data": [...]}. Each data entry: {"bucket": "2026-02-20T14:00:00", "allow_count": 10, "hold_count": 2, "block_count": 1, "total_amount": 25000.0, "blocked_amount": 5000.0}. Ordered by bucket descending (newest first).[TENANT] GET /v1/stats/entities
Returns the top 20 entities ranked by the number of non-allow verdicts (hold + block triggers).
curl -X GET https://froddy.io/v1/stats/entities \
-H "X-API-Key: your_key"
{"entities": [...]}. Each entry: {"entity_id": "partner_42", "total": 50, "allow_count": 40, "hold_count": 7, "block_count": 3, "total_amount": 125000.0, "blocked_amount": 15000.0, "last_seen": "2026-02-20T14:30:00"}. Ranking: block_count DESC, hold_count DESC, total DESC. The limit is parameterized: limit query parameter (default 20, max 100). Additional filters: tenant, scenario.RCL can send real-time alerts to a Telegram group or channel on every HOLD or BLOCK verdict. Note: Webhook and Email are the primary notification channels. Telegram is an optional additional channel that may be deprecated in favor of other messaging integrations.
[TENANT] GET /v1/telegram
Returns the current Telegram alert configuration for this tenant.
[TENANT] PUT /v1/telegram
curl -X PUT https://froddy.io/v1/telegram \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{
"bot_token": "123456:ABC-DEF...",
"chat_id": "-1001234567890"
}'
| Field | Type | Required | Description |
|---|---|---|---|
bot_token |
string | yes | Telegram Bot API token |
chat_id |
string | yes | Target chat/group/channel ID |
[TENANT] DELETE /v1/telegram
Disables Telegram alerts for this tenant.
Each alert includes: verdict emoji (🟡 hold / 🔴 block), rule name, entity ID, amount, human-readable reason, and timestamp.
🔴 RCL Alert: BLOCK
Entity: partner_42
Amount: $15,000.00
Rule: R-CEIL
Reason: daily ceiling exceeded: $53,000 / $50,000
Event: payout_12345
Time: 2026-02-20T14:30:01
Emoji mapping: 🔴 = block, 🟡 = hold-for-review.
RCL sends an HTTP POST request to a configured URL on every non-allow verdict.
[TENANT] GET /v1/webhook
[TENANT] PUT /v1/webhook
curl -X PUT https://froddy.io/v1/webhook \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://your-system.com/rcl-alert"
}'
| Field | Type | Required | Description |
|---|---|---|---|
webhook_url |
string | yes | HTTPS URL that will receive POST requests on hold/block |
[TENANT] DELETE /v1/webhook
For details on webhook delivery mechanics (payload format, retry policy, expected response), see Webhook Delivery.
Per-entity threshold overrides let you customize rule parameters for specific entities (e.g. VIP partners with legitimate high volumes) without changing the global policy.
Entity overrides can be managed both via the Policy API (entity_overrides field in the policy JSON) and via these dedicated endpoints.
[TENANT] GET /v1/entity-overrides/{entity_id}
Returns the current threshold overrides for a specific entity.
[TENANT] PUT /v1/entity-overrides/{entity_id}
curl -X PUT https://froddy.io/v1/entity-overrides/partner_vip \
-H "X-API-Key: your_key" \
-H "Content-Type: application/json" \
-d '{
"R-CEIL": { "daily_ceiling_usd": 200000 },
"R-COHORT": { "hold_usd": 50000, "block_usd": 300000 }
}'
The override object is a partial policy — only include the rules and parameters you want to override.
[TENANT] DELETE /v1/entity-overrides/{entity_id}
Removes all custom thresholds for the entity. It will be evaluated against the global policy.
entity_overrides key — they are the same store. Setting an override via this endpoint writes to policy.entity_overrides[entity_id] and increments the policy version. Responses:{"tenant": "...", "entity_overrides": {...}, "count": N}{"tenant": "...", "entity_id": "...", "overrides": {...}} (404 if not found){"tenant": "...", "entity_id": "...", "overrides": {...}, "policy_version": N}{"tenant": "...", "entity_id": "...", "deleted": true, "policy_version": N} (404 if not found)Re-evaluate historical decisions using alternative thresholds, without affecting the live policy. Use this to model the impact of threshold changes before committing them.
[TENANT] POST /v1/sensitivity
Request body:
{
"configs": [
{
"label": "conservative",
"R-COHORT": { "hold_usd": 15000, "block_usd": 50000 },
"R-CEIL": { "daily_ceiling_usd": 30000 },
"R-VEL": {}
},
{
"label": "permissive",
"R-COHORT": { "hold_usd": 50000, "block_usd": 200000 }
}
],
"scenario": "default"
}
Accepts 1–5 configs. Each config can include R-COHORT, R-CEIL, R-VEL thresholds. The scenario filter is optional. Currently, only R-COHORT (single-transaction size) is re-evaluated against historical data. R-CEIL and R-VEL require full state replay and are not re-simulated. Historical data is loaded up to 50,000 decisions.
Response:
{
"tenant": "demo",
"total_decisions": 1500,
"results": [
{
"label": "conservative",
"config": { "R-COHORT": { "hold_usd": 15000, "block_usd": 50000 }, ... },
"verdicts": { "allow": 1200, "hold": 250, "block": 50 },
"hold_rate": 16.67,
"block_rate": 3.33,
"note": "Only R-COHORT is re-evaluated..."
}
]
}
Aggregated data suitable for generating a pilot summary report.
[TENANT] GET /v1/report
Optional query parameter: scenario (string). No date range filter — returns all data for the tenant.
Response:
{
"tenant": "acme_corp",
"scenario": "default",
"policy": { "version": 5, "policy": {...}, "updated_at": "..." },
"report": {
"summary": { "total": 1500, "allow": 1200, "hold": 250, "block": 50 },
"rule_breakdown": [...],
"top_entities": [...],
"daily_timeline": [...]
},
"generated_at": "2026-02-20T15:00:00"
}
The data is structured for programmatic consumption (PDF generation, dashboards). The report object contains aggregated data from db.get_report_data().
[PUBLIC] GET /health
Returns the service and database health status. No authentication required.
curl https://froddy.io/health
{
"status": "ok",
"db_healthy": true,
"fail_open": true,
"service": "rcl-proto",
"mode": "shadow",
"version": "1.0.0",
"db_backend": "SQLite",
"uptime_s": 3600
}
status is "ok" when the database is healthy, "degraded" otherwise. fail_open indicates the service recommends clients proceed (allow) if RCL is unreachable.
These endpoints require the admin API key and are used for provisioning and managing tenants. They are not available to regular tenant API keys.
Note for external integrators: Tenant provisioning is handled by the Froddy team during pilot onboarding. These endpoints are documented here for completeness. You do not need to call them directly.
[ADMIN] POST /v1/tenants
curl -X POST https://froddy.io/v1/tenants \
-H "X-API-Key: admin_key" \
-H "Content-Type: application/json" \
-d '{
"slug": "acme_corp",
"name": "Acme Corporation"
}'
Response:
{
"tenant_slug": "acme_corp",
"api_key": "rcl_abc123..."
}
| Field | Type | Required | Description |
|---|---|---|---|
slug |
string | yes | URL-safe unique identifier for the tenant |
name |
string | yes | Human-readable tenant name |
[ADMIN] GET /v1/tenants
[ADMIN] DELETE /v1/tenants/{slug}
decisions table, then the tenant row (including webhook URL and Telegram config) is deleted from the tenants table. Policy history snapshots in the policy_history table are not deleted (orphaned). The demo tenant cannot be deleted (returns 400). Nonexistent slug returns 404.{"tenants": [{"tenant_slug": "...", "name": "...", "policy_version": N, "created_at": "..."}, ...]} (API keys are not returned in the list).{"deleted": "acme_corp"}.The /v1/evaluate endpoint is idempotent. If the same transaction is submitted more than once — identified by the combination of event_id + tenant + scenario — RCL returns the original verdict without re-evaluating.
This means:
event_id a second time will not produce a different result, even if the policy has changed since the first evaluation.event_id is evaluated exactly once per tenant.Recommended practice: Use your system's native payout identifier as event_id. This naturally ensures deduplication.
The idempotency scope is event_id + tenant + scenario. Decisions are stored indefinitely (no TTL) — an event_id that has been evaluated will always return the same verdict, even if the policy has changed since. The scenario field defaults to "default".
All errors return a JSON body with a detail field:
{
"detail": "entity_id is required"
}
| Status | Meaning |
|---|---|
200 |
Success |
400 |
Bad request — invalid or missing fields |
401 |
Unauthorized — missing or invalid API key |
403 |
Forbidden — valid key but insufficient permissions (e.g. tenant key on admin endpoint) |
404 |
Not found — endpoint or resource does not exist |
409 |
Conflict — possible for duplicate resource creation |
422 |
Unprocessable entity — valid JSON but semantically invalid |
429 |
Rate limit exceeded |
500 |
Internal server error |
The /v1/evaluate endpoint is designed for synchronous, inline use in the payout execution path. Response times are typically in the low double-digit millisecond range.
Client-side recommendations:
allow to avoid blocking payouts during a service outage. This is a client-side decision.The /v1/evaluate endpoint is rate-limited per tenant.
| Parameter | Default | Description |
|---|---|---|
| Limit | 100 requests/minute | Per tenant, sliding window |
| Response on exceed | HTTP 429 | {"detail": "Rate limit exceeded (100 req/min). Retry after a few seconds."} |
| Configuration | RCL_RATE_LIMIT env var | Configurable at deployment |
No rate limit headers are currently returned. The limit applies to /v1/evaluate only; other endpoints are not rate-limited.
The API is versioned via URL path prefix: all current endpoints are under /v1/.
/v2/)./v2/). No formal deprecation timeline is defined at this stage. Clients integrating with /v1/ can expect stability for the duration of their pilot and production use.When a webhook URL is configured, RCL sends a POST request to that URL on every hold or block verdict.
The webhook receives a JSON POST with Content-Type: application/json:
{
"event_id": "payout_12345",
"entity_id": "partner_42",
"amount": 15000.0,
"verdict": "hold-for-review",
"rule_id": "R-CEIL",
"reason": "daily ceiling exceeded: $53,000 / $50,000",
"evaluated_at": "2026-02-20T14:30:01"
}
The payload is identical for both webhook and Telegram alerts. Field rule_id (not rule) matches the evaluate response schema.
/v1/evaluate response.http:// or https://. HTTPS is strongly recommended for production.Every endpoint falls into one of three access levels:
| Level | Authentication | Who Uses It |
|---|---|---|
[PUBLIC] |
None | Anyone — health checks, demo |
[TENANT] |
Tenant API key via X-API-Key |
Integrating systems, ops teams |
[ADMIN] |
Admin API key via X-API-Key |
Froddy team — tenant provisioning |
| Method | Endpoint | Access | Description |
|---|---|---|---|
| POST | /v1/evaluate |
TENANT | Evaluate a transaction |
| GET | /v1/decisions |
TENANT | Query decision log |
| GET | /v1/decisions/export |
TENANT | Export decisions (up to 50K) |
| GET | /v1/policy |
TENANT | Get current policy |
| PUT | /v1/policy |
TENANT | Update policy |
| GET | /v1/policy/history |
TENANT | List policy versions |
| POST | /v1/policy/rollback/{ver} |
TENANT | Rollback to a policy version |
| GET | /v1/rules |
TENANT | Get active rules |
| GET | /v1/stats |
TENANT | Aggregated statistics |
| GET | /v1/stats/timeseries |
TENANT | Verdict counts over time |
| GET | /v1/stats/entities |
TENANT | Top 20 entities by triggers |
| GET | /v1/telegram |
TENANT | Get Telegram config (optional) |
| PUT | /v1/telegram |
TENANT | Set Telegram config (optional) |
| DELETE | /v1/telegram |
TENANT | Remove Telegram config (optional) |
| GET | /v1/webhook |
TENANT | Get webhook config |
| PUT | /v1/webhook |
TENANT | Set webhook config |
| DELETE | /v1/webhook |
TENANT | Remove webhook config |
| GET | /v1/entity-overrides/{id} |
TENANT | Get entity override |
| PUT | /v1/entity-overrides/{id} |
TENANT | Set entity override |
| DELETE | /v1/entity-overrides/{id} |
TENANT | Remove entity override |
| POST | /v1/sensitivity |
TENANT | What-if analysis |
| GET | /v1/report |
TENANT | Pilot report data |
| GET | /health |
PUBLIC | Service health |
| POST | /v1/tenants |
ADMIN | Create tenant |
| GET | /v1/tenants |
ADMIN | List tenants |
| DELETE | /v1/tenants/{slug} |
ADMIN | Delete tenant |
Froddy RCL API Reference · v1 · froddy.io · Product Docs · Interactive Demo