Deception is not new in cybersecurity. Canary tokens and honeypots have been around for years. The trappsec concept of embedding deception inside APIs is something that hasn't been tried yet — but is the need of the hour, especially considering that a growing number of breaches can be attributed to exploitation of APIs and business logic.

Trappsec provides two primitives: decoy routes and honey fields.

In this article, we will only talk about decoy routes. At the core, it's about placing a fake endpoint which when interacted with, raises an alert.

A good trap must blend into your application like a boring, standard — perhaps even tedious part of your API. It must also behave like a real API. If real APIs react differently to authentication status of the caller, so must the traps.

Consider a subscription upgrade decoy that appears to accept a plan change. This is what the decoy configuration would look like in a python application — that indicates a responses that states that the operation was not permitted. Nothing here looks like a honeypot, nothing leaks implementation detail. (for ways to lure attackers to this, read baiting and lures)

ts.trap("/api/subscription/upgrade") \
 .methods("POST") \
 .intent("Entitlement Manipulation") \
 .respond(403, {"error": "Upgrade not permitted"})

But when someone could try probing this using the following curl command, the output might be something unexpected like (401, unauthorized) instead of the upgrade not permitted error.

$ curl -X POST https://api.example.com/api/subscription/upgrade \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "enterprise",
    "billing_cycle": "annual",
    "source": "self_service"
  }'

HTTP/1.1 401 Unauthorized
{
  "error": "Authentication required"
}

This is because the trappsec framework can lookup the authentication context of every request that interacts with a decoy route. It lets you define a global response as well as endpoint specific defaults like in the below example:

ts.trap("/api/subscription/upgrade") \
    .methods("POST") \
    .intent("Entitlement Manipulation") \
    .if_unauthenticated(401, {"error": "Authentication required"})
    .respond(403, {"error": "Upgrade not permitted"}) \

A 401 isn't a dead end — it's a fork. It asks the requester to make a choice. Retry with credentials, or move on.

That choice is meaningful.

$ curl -X POST https://api.example.com/api/subscription/upgrade \
  -H "Authorization: Bearer eyJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "enterprise",
    "billing_cycle": "annual",
    "source": "self_service"
  }'

HTTP/1.1 403 Forbidden
{
  "error": "Upgrade not permitted"
}

When someone authenticates and comes back, they're no longer anonymous noise. They've crossed an identity boundary. That's where API-level deception becomes useful — providing a high confidence alert with all the right context that make it easy for security teams to respond.

// The Alert
{
  "timestamp": 1707135273.482,
  "event": "trappsec.trap_hit",
  "type": "alert",
  "path": "/api/subscription/upgrade",
  "method": "POST",
  "user_agent": "curl/8.5.0",
  "ip": "203.0.113.42",
  "app": {
    "service": "billing-api",
    "environment": "production",
    "hostname": "worker-03"
  },
  "user": "user_8f3a2c",
  "role": "user",
  "intent": "entitlement_manipulation"
}

Note: trappsec alerts are only created for authenticated interactions. Unauthenticated interactions are logged as signals instead of alerts. their usage in detection or investigation is left to the discretion of the monitoring team.