Indicators
Indicator (scanner) catalogue, per-member access, and backoffice grant/revoke
Manage the indicators (scanners) 1House offers, and who holds access to them on TradingView.
Two halves, kept apart
The catalogue is what can be granted — one row per indicator, carrying the TradingView pineId that automation is keyed on. Access is who actually holds each one. Deleting an indicator is a catalogue decision, never a mass revocation, which is why the two are separate.
Overview
The Indicators API provides:
- Indicator catalogue CRUD (admin)
- Per-indicator access rosters, grants and revocations (admin)
- A worklist of grants that need a human
- Backoffice grant/revoke of a customer's entire indicator set
- The marketplace request funnel that feeds all of it
Base Path: /v1/streams/scanners
Marketplace: /v1/streams/marketplace
Backoffice webhooks: /v1/streams/webhooks
Indicators and scanners are the same thing
The product calls them indicators; the API paths and payload fields call them scanners. They refer to the same records — no translation is needed beyond the path.
Access lifecycle
Every grant is in exactly one state. Nothing moves between them except through the endpoints below or the automated sweep.
| Status | Meaning |
|---|---|
pending_grant | Queued; the TradingView call has not succeeded yet |
active | The member holds the indicator on TradingView |
pending_revoke | Queued for removal, or counting down a grace period |
revoked | Removed on TradingView |
failed | Retries are spent — waiting for a human |
source decides what automation is allowed to touch a grant:
| Source | Behaviour |
|---|---|
manual | A comped or staff grant. Never touched by the automated revoke sweep |
automated | Created by the backoffice. Follows the member's activity status |
marketplace | Came out of an approved marketplace access request |
A grant needs a TradingView username
TradingView is the system of record for who can open an invite-only script, and it knows people only by their TradingView handle. A grant without one cannot be pushed, so every grant endpoint either takes a tradingviewUsername or resolves one from an existing grant or marketplace request.
Backoffice: Grant or Revoke All Indicators
Grant or revoke every active indicator for one customer, identified by the backoffice's own customer_id.
POST /v1/streams/webhooks/indicator-accessThis endpoint does not use an API key
Like the activity-status webhook, this is a server-to-server call from the external backoffice. It authenticates with the X-Webhook-Secret header alone — no X-API-Key, no JWT — and it is refused outright (503) when no secret is configured on the service.
Headers:
| Header | Required | Description |
|---|---|---|
| X-Webhook-Secret | Yes | Shared secret (WEBHOOK_SECRET), same value the activity-status webhook uses |
| Content-Type | Yes | application/json |
Request Body:
{
"customer_id": "CUS-10482",
"action": "grant",
"tradingview_username": "trader_jane"
}Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| customer_id | string | Yes | The backoffice's customer id. Matched against users.customerId. customerId is also accepted |
| action | string | Yes | grant or revoke. Anything else is a 400 — the value is never guessed at |
| tradingview_username | string | Only when granting and none is on record | The member's TradingView handle. tradingviewUsername is also accepted |
Grant is refused without a TradingView handle
When granting, the service looks for a handle in three places, in order: the request body, any grant this person already holds, then their most recent marketplace access request. If none of them has one, the request is refused with 422 TRADINGVIEW_USERNAME_REQUIRED and nothing is written — rather than creating a row per indicator that is guaranteed to fail. Retry with tradingview_username once you have it.
Behaviour:
- Grant walks the active catalogue and grants each indicator with
source: "automated", which leaves the grants inside the reach of the activity-status sweep. A grant the member already holds is refreshed, not duplicated. - Revoke walks the rows the member actually holds rather than the catalogue, so an indicator that has since been retired is still taken away. Revocation is immediate — there is no grace period here, unlike a lapsed subscription.
- Both are idempotent. Re-sending the same call is always safe.
Try it out:
Example Request:
curl -X POST "https://api-gateway.dev.1houseglobalservices.com/v1/streams/webhooks/indicator-access" \
-H "X-Webhook-Secret: your-webhook-secret" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "CUS-10482",
"action": "grant",
"tradingview_username": "trader_jane"
}'const response = await fetch(
'https://api-gateway.dev.1houseglobalservices.com/v1/streams/webhooks/indicator-access',
{
method: 'POST',
headers: {
'X-Webhook-Secret': process.env.STREAM_WEBHOOK_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer_id: 'CUS-10482',
action: 'grant',
tradingview_username: 'trader_jane',
}),
}
);
const data = await response.json();import os
import requests
url = "https://api-gateway.dev.1houseglobalservices.com/v1/streams/webhooks/indicator-access"
headers = {
"X-Webhook-Secret": os.environ["STREAM_WEBHOOK_SECRET"],
"Content-Type": "application/json",
}
payload = {
"customer_id": "CUS-10482",
"action": "grant",
"tradingview_username": "trader_jane",
}
response = requests.post(url, headers=headers, json=payload)
result = response.json()Example Response:
{
"success": true,
"message": "Indicator access granted",
"data": {
"customerId": "CUS-10482",
"action": "grant",
"userId": "68f2c1a4e9b21c0012aa77d1",
"email": "jane@example.com",
"tradingviewUsername": "trader_jane",
"scanners": 5,
"changed": 5,
"skipped": 0,
"failed": 0,
"results": [
{
"scannerId": "68a1f0c2d3b45e0011bc2201",
"scannerName": "ZEUS Precision Levels",
"accessId": "68f3aa19d3b45e0011bc9911",
"status": "active"
}
]
}
}Response Fields:
| Field | Type | Description |
|---|---|---|
| scanners | number | Indicators in the active catalogue |
| changed | number | Grants or revocations that went through |
| skipped | number | Catalogue entries the member did not hold (revoke only) |
| failed | number | Rows that exhausted their retries; they appear on the failures worklist |
| results | array | Per-indicator outcome, including error when one failed |
Error Responses:
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_PAYLOAD | customer_id is missing |
| 400 | INVALID_ACTION | action was not grant or revoke |
| 401 | UNAUTHORIZED | Wrong or missing X-Webhook-Secret |
| 404 | CUSTOMER_NOT_FOUND | No account is mapped to that customer_id — map it on the account first |
| 422 | TRADINGVIEW_USERNAME_REQUIRED | Granting, and no TradingView handle is on record. Nothing was written |
| 503 | WEBHOOK_NOT_CONFIGURED | WEBHOOK_SECRET is not set on the service |
Backoffice: Activity Status
Drives grant/revoke automatically as a member's subscription comes and goes. Going inactive starts a grace period rather than revoking outright, so a failed card does not immediately cost somebody their indicators. manual grants are never touched.
POST /v1/streams/webhooks/activity-statusRequest Body:
{
"userId": "68f2c1a4e9b21c0012aa77d1",
"status": "inactive"
}| Parameter | Type | Description |
|---|---|---|
| userId | string | The member's user ObjectId. Either this or email is required |
| string | The member's email, when the user id is not known | |
| active | boolean | true / false. Use this or status |
| status | string | One of active, enabled, inactive, cancelled, canceled, expired, suspended, disabled |
Unrecognised status is rejected, never guessed
A word this endpoint does not recognise returns 400 UNKNOWN_STATUS. Defaulting an unreadable value to "inactive" would revoke paid access for real people, so the failure is deliberately loud.
Indicator Catalogue
List Indicators
GET /v1/streams/scanners| Parameter | Type | Description |
|---|---|---|
| includeInactive | boolean | true to include retired indicators (admin views only) |
Each row carries activeAccessCount — a head count, so a list can say "12 people" without fetching every roster.
Example Response:
{
"success": true,
"data": [
{
"id": "68a1f0c2d3b45e0011bc2201",
"name": "ZEUS Precision Levels",
"description": "Session-based precision levels",
"pineId": "PUB;7a1f9c2e4b8d",
"tradingviewUrl": "https://www.tradingview.com/script/7a1f9c2e/",
"isActive": true,
"sortOrder": 10,
"activeAccessCount": 128
}
]
}Get Indicator
GET /v1/streams/scanners/:idCreate Indicator (Admin)
POST /v1/streams/scanners{
"name": "ZEUS Precision Levels",
"description": "Session-based precision levels",
"pineId": "PUB;7a1f9c2e4b8d",
"tradingviewUrl": "https://www.tradingview.com/script/7a1f9c2e/",
"isActive": true,
"sortOrder": 10
}pineId is what makes automation possible
pineId is TradingView's own identifier for the script — the PUB;<hash> form shown in the invite-only script's URL. An indicator without one can be listed, but never granted automatically.
Update Indicator (Admin)
PUT /v1/streams/scanners/:idDelete Indicator (Admin)
DELETE /v1/streams/scanners/:idDeleting will not strand people
An indicator anyone still holds, or that a marketplace product still points at, cannot be deleted — the request returns 400 telling you how many. Deactivate it instead (isActive: false), which keeps every access row meaningful and lets an admin revoke deliberately.
Per-Indicator Access (Admin)
List Access
GET /v1/streams/scanners/:id/access| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by access status (see the lifecycle table above) |
| page | number | Page number (default 1) |
| limit | number | Rows per page (default 50, max 200) |
Grant Access
POST /v1/streams/scanners/:id/access{
"userId": "68f2c1a4e9b21c0012aa77d1",
"tradingviewUsername": "trader_jane"
}| Parameter | Type | Required | Description |
|---|---|---|---|
| userId | string | One of userId / email | The member's user ObjectId |
| string | One of userId / email | The member's email | |
| tradingviewUsername | string | Yes | TradingView handle to grant on |
Grants made here are source: "manual" — deliberately immune to the automated revoke sweep, which is what makes them usable for comped and staff access.
Revoke Access
DELETE /v1/streams/scanners/:id/access/:accessIdImmediate, with no grace period.
Retry a Failed Job
POST /v1/streams/scanners/:id/access/:accessId/retryResumes the job that failed rather than reversing it — a retry of a failed revoke revokes, it does not grant.
Failures Worklist
Every grant needing a human, across every indicator.
GET /v1/streams/scanners/access-failuresEach row carries lastError and attempts, so a stuck grant waits visibly for somebody rather than disappearing.
Marketplace
Where a member asks for an indicator in the first place. An approved request for a scanner product is what creates the grant — see Per-Indicator Access for what happens next.
Base Path: /v1/streams/marketplace
Full reference lives on its own page
Marketplace documents the whole surface — authoring and editing products, the custom request-form builder, product image uploads, and the review queue. What follows is only the part of the funnel that ends in an indicator grant.
List Products
GET /v1/streams/marketplace/productsThe public catalogue is active products only, and no parameter widens it — the admin listing is a separate route, GET /v1/streams/marketplace/products/all.
| Parameter | Type | Description |
|---|---|---|
| productType | string | scanner or misc |
| Field | Type | Description |
|---|---|---|
| productType | string | scanner products carry an entitlement; misc products do not |
| scannerId | string | null | Which indicator an approval grants. Only ever set on scanner products |
Submit an Access Request
POST /v1/streams/marketplace/access-requestsThe form is the contract
This endpoint accepts what the 1hstream marketplace form collects — name, email, TradingView handle — plus answers to any custom questions the product defines, and requires the same fields the form marks required. Nothing else is read from the body.
{
"productId": "68a1f0c2d3b45e0011bc3301",
"fullName": "Jane Ruiz",
"email": "jane@example.com",
"tradingviewId": "trader_jane"
}| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Yes | The product being requested |
| fullName | string | Yes | Trimmed before storing |
| string | Yes | Lower-cased before storing; must look like an email address | |
| tradingviewId | string | For scanner products | The member's TradingView handle |
A scanner request without a handle is refused
tradingviewId is what an approval is eventually delivered with, so a request for a scanner product is rejected with 400 without one. Accepting it would move the failure to the moment an admin clicks Approve, where it reads as the approval breaking rather than the form being incomplete. misc products carry no entitlement and leave the field optional.
Re-submitting updates, it does not duplicate
A second request for the same product and email updates the pending or denied one — that is how somebody corrects a mistyped TradingView handle. A request that has already been approved is returned untouched: its details have been acted on, and rewriting them would desync the grant from the record that produced it.
Try it out:
Legacy fields
Requests taken before the form settled may carry a phone or message. Both are still returned so a reviewer can read them, but neither is collected or accepted any more.
Check Request Status
GET /v1/streams/marketplace/access-requests/status?email=jane@example.comReturns a { [productId]: status } map, picking approved over anything else when a member has more than one request for the same product.
Review Requests (Admin)
GET /v1/streams/marketplace/access-requests
PATCH /v1/streams/marketplace/access-requests/:idPATCH takes { "status": "approved" | "denied" | "pending" }. For a scanner product the decision is the entitlement: approving creates or revives the access row and queues the TradingView grant; withdrawing an approval queues the revoke, and only for the row this request created — a separate manual grant to the same person survives.
A decision is never lost to a third party
If the TradingView call cannot be made, the decision is still saved and the response carries an entitlementWarning. The grant lands in failed and appears on the failures worklist. Surface the warning rather than treating the call as a plain success.
Related Documentation
- Marketplace — authoring the products that feed these requests
- Live Streams — schedules and streaming sessions from the same service
- Trading — trade ideas, which reference indicators by
indicatorId - Authentication — mapping an account to a backoffice
customerId