Skip to content

Webhook Security

Webhooks are the primary attack surface — they accept requests from the public internet with no browser context, no cookies, and no CSRF tokens. Any HTTP client can send a POST to the webhook URL. 4pass implements 4 mandatory validation layers, each of which must pass before an order is executed. A failure at any layer terminates the request immediately.


Validation Flow

flowchart TB
    REQ["Incoming Webhook Request"] --> L1{"Level 1<br/>URL Token"}
    L1 -->|Invalid| R404["404"]
    L1 -->|Valid| L2{"Level 2<br/>Timestamp"}
    L2 -->|Expired| R401a["401"]
    L2 -->|Valid| L3{"Level 3<br/>Secret"}
    L3 -->|Wrong| R401b["401 + Alert"]
    L3 -->|Valid| L4{"Level 4<br/>Replay"}
    L4 -->|Duplicate| R409["409"]
    L4 -->|Unique| EXEC["Order Execution"]

Each layer serves a distinct security purpose and returns a different HTTP status code, enabling precise attack classification in logs and monitoring.


Level 1: Unique Token per Account

The webhook URL itself contains an opaque token that identifies both the user and the specific trading account:

POST /webhook/{account_webhook_token}/order
Property Detail
Token format 32-byte URL-safe string (secrets.token_urlsafe(32))
Entropy 256 bits
Scope Identifies both the user AND the specific trading account
Lookup Token → TradingAccountUser (single DB query with join)
Invalid token 404 Not Found — does not reveal whether the account exists
Disabled user 403 Forbidden — account exists but user is deactivated

Why 404 Instead of 401?

Returning 404 Not Found for invalid tokens prevents account enumeration. An attacker cannot distinguish between "this token doesn't exist" and "this URL path doesn't exist." A 401 would confirm the endpoint exists and invite further probing.


Level 2: Timestamp Validation

Every real-trading webhook request must include a timestamp. The server validates that the request was generated recently, preventing old or captured requests from being replayed hours or days later.

Property Detail
Payload field "timestamp": "{{timenow}}" (TradingView built-in variable)
Format UTC timestamp string, parsed server-side
Validation |server_time - request_timestamp| < max_request_age
Default max age 300 seconds (5 minutes)
Configurable Per-account max_request_age setting
Failure 401 Unauthorized + webhook_timestamp_expired audit event + email alert
Simulation mode Bypassed — simulation orders don't require timestamps
request_age = abs(time.time() - timestamp.timestamp())
if request_age > ctx.max_request_age:
    raise HTTPException(status_code=401, detail="Request expired")

Clock Skew

The 300-second window accounts for network latency and minor clock differences between TradingView's servers and 4pass. The abs() also handles the case where TradingView's clock is slightly ahead. Users experiencing persistent timestamp failures can increase max_request_age in their account settings.


Level 3: Secret Verification

Each trading account has a unique webhook secret that must be included in the request body. This authenticates the sender — even if an attacker discovers the webhook URL (Level 1), they cannot execute orders without the secret.

Property Detail
Payload field "secret": "your-webhook-secret-here"
Secret format 32-byte URL-safe string (secrets.token_urlsafe(32))
Entropy 256 bits
Comparison hmac.compare_digest(secret, ctx.webhook_secret)constant-time
Failure 401 Unauthorized + webhook_credential_attack audit event + email alert
Rotation Users can regenerate their webhook secret from the dashboard at any time

Why Constant-Time Comparison?

A naive secret == expected comparison leaks information through timing. If the first character matches, the comparison takes slightly longer than if it doesn't. An attacker can brute-force the secret one character at a time by measuring response times. hmac.compare_digest() compares all bytes in constant time regardless of where they differ.


Level 4: Replay Detection

Even with a valid token, timestamp, and secret, an attacker who captures a legitimate request could replay it within the timestamp window. Level 4 prevents this by tracking request fingerprints in Redis.

Property Detail
Hash input user_id:action:symbol:quantity:timestamp:secret
Algorithm SHA-256, truncated to 32 hex chars
Storage Redis key: webhook:replay:{hash}
TTL max_request_age seconds (matches timestamp window)
Duplicate check redis.exists(key) — if key exists, request is a replay
Failure 409 Conflict + webhook_replay_attack audit event + email alert
flowchart TB
    REQ["Webhook Request"] --> HASH["SHA-256 hash of:<br/>user_id:action:symbol:quantity:timestamp:secret"]
    HASH --> CHECK{"Redis key<br/>exists?"}
    CHECK -->|"Yes"| REJECT["409 Conflict<br/>Replay detected"]
    CHECK -->|"No"| STORE["Store hash in Redis<br/>TTL = max_request_age"]
    STORE --> PROCEED["Proceed to<br/>order execution"]

The hash includes the secret field to prevent an attacker from crafting a different request with the same hash. The TTL automatically expires old hashes, keeping Redis memory bounded.


Additional Protections

Beyond the 4-layer validation, webhook requests pass through additional checks before order execution:

Protection Implementation Failure
Per-user rate limiting 60 requests/minute via Redis sorted set sliding window 429 Too Many Requests
IP whitelist Optional per-user restriction (user_allowed_ips table) 403 Forbidden + webhook_ip_blocked alert
Subscription check Free plan users restricted to simulation only 403 Forbidden
Daily order limit Plan-based maximum orders per day (prevents runaway trading) 429 Too Many Requests
Credential verification Reject if broker credentials haven't been verified 400 Bad Request
Terms of Service Reject if user hasn't accepted current ToS 403 Forbidden
Audit logging All attempts (success and failure) logged with IP, User-Agent, full context

Rate Limiting is Fail-Open

The per-user rate limiter uses Redis but fails open on Redis connection errors. This design ensures that a temporary Redis outage doesn't block legitimate trading operations. WAF-level rate limiting (per-IP, AWS-managed) provides a backstop.


TradingView Configuration

Webhook URL

https://4pass.io/webhook/{account_webhook_token}/order

Replace {account_webhook_token} with the token shown in your dashboard under Trading Accounts → Webhook Settings.

Alert Message (JSON Payload)

{
    "action": "{{strategy.order.alert_message}}",
    "symbol": "TXFR1",
    "quantity": {{strategy.order.contracts}},
    "timestamp": "{{timenow}}",
    "secret": "your-webhook-secret-here"
}
Field Source Description
action {{strategy.order.alert_message}} TradingView strategy action (buy, sell, close, etc.)
symbol Hardcoded or {{ticker}} The instrument to trade
quantity {{strategy.order.contracts}} Number of contracts/lots
timestamp {{timenow}} Current UTC time (TradingView built-in)
secret Your webhook secret Found in dashboard → Trading Accounts → Webhook Settings

Simulation Mode

Append ?simulation=true to the webhook URL to execute in simulation mode. Simulation orders bypass timestamp and secret validation (Levels 2-3) and do not execute real trades.

https://4pass.io/webhook/{token}/order?simulation=true

Security Comparison

How 4pass webhook security compares to common alternatives:

Feature 4pass Typical Webhook API Key Only
URL-based account isolation Yes Per-account token No Shared URL No
Timestamp validation Yes Configurable window No No
Secret authentication Yes Constant-time compare No or basic auth Yes Header
Replay detection Yes Redis + SHA-256 No No
Per-user rate limiting Yes 60/min sliding window No Per-key
IP whitelisting Yes Optional per-user No No
Real-time attack alerts Yes Email with context No No
Audit logging Yes Full request context No Partial