API reference

Version 1 of the Argus API: what you spend, where, and what is about to run out, plus the writes to connect providers, send events, set alerts and record negotiated pricing. Base URL https://api.argus.town. This page is generated from the same contract as the OpenAPI document, so it cannot describe an endpoint that does not exist.

Start

Create an api key in Settings (or have an agent request one through the device login), then:

curl -s https://api.argus.town/v1/project -H "Authorization: Bearer $ARGUS_API_KEY"

Responses are bare resources with an object tag; lists are {"object": "list", "data", "has_more", "next_cursor"} and take limit and cursor. Unknown query parameters and body fields are rejected with the field named. Within v1 changes only add fields: ignore the ones you do not recognise.

Ids

Ids carry their kind: prj_ project, conn_ connection, key_ key, sync_ sync, plan_ plan, src_ source, lnk_ connect link, alr_ alert rule, fire_ alert firing. Input also accepts the bare form.

Idempotency

Operations marked idempotent accept an Idempotency-Key header of 1 to 255 printable ASCII characters. A retry with the same key and body returns the first response with Idempotent-Replayed: true for 24 hours; the same key with a different body, or while the first request is still running, is a 409 (idempotency_key_reused, idempotency_key_in_progress). Keys are scoped to the project.

Rate limits

600 a minute per key, and 6,000 a minute events per source on POST /v1/events. Over the limit is a 429 with Retry-After and a RateLimit-Policy header. More than 30 failed key checks a minute from one address are refused with too_many_auth_failures.

Errors

Every failure has a real HTTP status and one shape. code is stable; param names the offending field; next_step, when present, says what to do. The full catalogue is on the errors page and at GET /v1/errors.

{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "Invalid value for \"limit\". Expected an integer from 1 to 200.",
    "param": "limit",
    "doc_url": "https://argus.town/docs/errors#invalid_parameter",
    "request_id": "…"
  }
}

MCP

The same operations are MCP tools at POST https://mcp.argus.town, named by their operation id and taking the same arguments, errors and idempotency. An API key can call every tool; an MCP key keeps what it has always had: reads, connecting a provider, connect links and refreshes. Session-only operations, the device login and event sending are never tools. See MCP.

Start

GET/v1No key

API index. Self-describing entry point: where the contract, error catalog and MCP endpoint live, how to authenticate, and the first authenticated call.

curl -s "https://api.argus.town/v1"
# index → 200
{
  "object": "api",
  "version": "v1",
  "status": "ok",
  "links": {
    "documentation": "https://argus.town/docs/api",
    "openapi": "/v1/openapi.json",
    "errors": "/v1/errors",
    "providers": "/v1/providers",
    "mcp": "https://mcp.argus.town"
  },
  "authentication": {
    "scheme": "bearer",
    "key_type": "api",
    "create_key_url": "https://argus.town/settings"
  },
  "first_call": {
    "method": "GET",
    "href": "/v1/project",
    "curl": "curl -s https://api.argus.town/v1/project -H \"Authorization: Bearer $ARGUS_API_KEY\""
  },
  "next_steps": [
    {
      "rel": "list_providers",
      "method": "GET",
      "href": "/v1/providers",
      "why": "See which providers Argus covers, no key needed."
    }
  ]
}
POST/v1/device/codeNo key

Start device login. For an agent with no credentials: ask for access, show the human `verification_uri_complete`, and poll `POST /v1/device/token`. The human signs in, picks the project and approves the scopes listed; nothing is issued before that. `api` (alias `read`) is an API key, which can read spend and also create and update connections, alert rules and negotiated pricing in the approved project; `ingest` is a telemetry source; `mcp` is an MCP key. Without `scopes`: `api` and `ingest`. Rate limited per address.

Optional client name and scopes.

client_namestringoptional

a string at most 60 character(s) long

scopes"api" | "read" | "ingest" | "mcp"[]optional

`api` (alias `read`): an API key; `ingest`: a telemetry source; `mcp`: an MCP key. Default: `api` and `ingest`.

Errors: invalid_request_body, invalid_parameter, payload_too_large

curl -s -X POST "https://api.argus.town/v1/device/code" -H "content-type: application/json" -d '{"client_name":"my-agent","scopes":["api","ingest"]}'
# api and ingest → 201
{
  "object": "device_code",
  "device_code": "argus_dev_3b9f0c5e1a7d4826b0c9e3f5a1d7b2c84e6f0a9d3c5b7e1f",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://argus.town/device",
  "verification_uri_complete": "https://argus.town/device?code=BCDF-GHJK",
  "expires_in": 900,
  "interval": 7
}
# unknown scope → 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "Invalid value for \"scopes[0]\": Expected \"api\" | \"read\" | \"ingest\" | \"mcp\", actual \"admin\"",
    "param": "scopes[0]",
    "doc_url": "https://argus.town/docs/errors#invalid_parameter",
    "request_id": null
  }
}
POST/v1/device/tokenNo key

Poll device login. Poll with the `device_code` until `status` leaves `pending`. On `approved` the credentials are returned once (`api_key`, `source.secret`, `mcp_key` as requested) together with the project; later polls say `consumed`. `denied`, `expired`, `consumed` and `invalid` are final: start again. Rate limited per code and, for unknown codes, per address.

The device code from the first step.

device_codestringrequired

a string at most 200 character(s) long

Errors: invalid_request_body, invalid_parameter, missing_parameter, payload_too_large

curl -s -X POST "https://api.argus.town/v1/device/token" -H "content-type: application/json" -d '{"device_code":"{device_code.device_code}"}'
# pending → 200
{
  "object": "device_token",
  "status": "pending",
  "next_steps": [
    {
      "rel": "poll",
      "method": "POST",
      "href": "/v1/device/token",
      "why": "Not approved yet. Poll again in 7 seconds."
    }
  ]
}
GET/v1/openapi.jsonNo key

OpenAPI document. This contract as OpenAPI 3.1, generated from the operation table.

GET/v1/errorsNo key

Error catalog. Every error code the API returns, with its type and HTTP status.

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

Errors: invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/errors?limit=1"
# first code → 200
{
  "object": "list",
  "data": [
    {
      "object": "error_code",
      "code": "connection_not_found",
      "type": "not_found",
      "status": 404,
      "description": "No connection with this id in the key's project.",
      "doc_url": "https://argus.town/docs/errors#connection_not_found"
    }
  ],
  "has_more": true,
  "next_cursor": "WyJjb25uZWN0aW9uX25vdF9mb3VuZCJd"
}

Providers

GET/v1/providersNo keyMCP tool providers_list

List providers. The provider catalog: pricing model and whether Argus can poll the provider with a credential. Public, no key.

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

connectablequery · booleanoptional

Only providers Argus can poll (true) or only telemetry-only ones (false).

Errors: invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/providers?connectable=true&limit=1"
# connectable → 200
{
  "object": "list",
  "data": [
    {
      "object": "provider",
      "id": "openrouter",
      "pricing_model": "usage_drawdown",
      "summary": "Prepaid credits drawn down per request across many model providers.",
      "connectable": true,
      "credential_types": [
        "api_key"
      ]
    }
  ],
  "has_more": true,
  "next_cursor": "WyJvcGVucm91dGVyIl0"
}
GET/v1/providers/{slug}No keyMCP tool providers_get

Get a provider. One provider with its full offering: plans, rate limits and how spend is observed.

slugpathrequired

Provider slug, for example `openrouter`.

Errors: provider_not_found

curl -s "https://api.argus.town/v1/providers/openrouter"
# openrouter → 200
{
  "object": "provider",
  "id": "openrouter",
  "pricing_model": "usage_drawdown",
  "summary": "Prepaid credits drawn down per request across many model providers.",
  "connectable": true,
  "credential_types": [
    "api_key"
  ],
  "offering": {
    "provider": "openrouter"
  }
}
# unknown → 404
{
  "error": {
    "type": "not_found",
    "code": "provider_not_found",
    "message": "No provider has the slug nope.",
    "param": null,
    "doc_url": "https://argus.town/docs/errors#provider_not_found",
    "next_step": {
      "hint": "List provider slugs.",
      "method": "GET",
      "href": "/v1/providers"
    },
    "request_id": null
  }
}

Project

GET/v1/projectAPI keyMCP tool project_get

Get the key's project. Who am I: the project this key belongs to, the key itself, what it may call, the plan, and what to do next.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed

curl -s "https://api.argus.town/v1/project" -H "Authorization: Bearer $ARGUS_API_KEY"
# empty project → 200
{
  "object": "project",
  "id": "prj_org_3kQ9vXw2",
  "name": "Acme",
  "created_at": "2026-10-01T09:30:00.000Z",
  "key": {
    "object": "key",
    "id": "key_7d2e91a0",
    "type": "api",
    "name": "ci",
    "prefix": "argus_api_ab12"
  },
  "permissions": [
    "project_get",
    "summary_get",
    "connections_list",
    "connections_get"
  ],
  "plan": {
    "id": "free",
    "name": "Free"
  },
  "next_steps": [
    {
      "rel": "list_providers",
      "method": "GET",
      "href": "/v1/providers?connectable=true",
      "why": "No connections yet. See which providers Argus can poll."
    }
  ]
}

Spend

GET/v1/summaryAPI keyMCP tool summary_get

Spend summary. What the project is spending and what is about to run out: month-to-date and projected totals, every connection with its metrics, and advisories.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed

curl -s "https://api.argus.town/v1/summary" -H "Authorization: Bearer $ARGUS_API_KEY"
# one connection → 200
{
  "object": "summary",
  "totals": {
    "mtd_spend_usd": 18.4,
    "projected_month_end_usd": 55.2
  },
  "connections": [
    {
      "object": "connection",
      "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
      "provider": "openrouter",
      "origin": "poll",
      "label": "OpenRouter (production)",
      "credential_type": "api_key",
      "status": "active",
      "status_detail": null,
      "last_synced_at": "2026-10-01T09:30:00.000Z",
      "created_at": "2026-10-01T09:30:00.000Z",
      "updated_at": "2026-10-01T09:30:00.000Z",
      "metrics": {
        "balance_usd": 42.5,
        "burn_per_day": 3.1,
        "days_to_zero": 13.7,
        "mtd_spend_usd": 18.4,
        "spend_basis": "reported",
        "observed_since": null,
        "balance_scope": "account",
        "projected_month_end_usd": 55.2,
        "wow_change_pct": 4.2,
        "rate_limit_headroom_pct": 80,
        "error_rate_pct": 0.4,
        "accuracy": "exact",
        "cost_drift_pct": 0.3,
        "billing_mode": "usage_drawdown",
        "utilization_pct": null,
        "used_amount": null,
        "included_amount": null,
        "included_meter_id": null,
        "subscription_fee_usd": null,
        "effective_usd_per_unit": null,
        "days_to_reset": null,
        "recommended_action": null,
        "recommended_tier": null,
        "recommended_delta_usd": null
      },
      "metered": true
    }
  ],
  "advisories": [],
  "next_steps": []
}
GET/v1/spendAPI keyMCP tool spend_get

Spend over time. Daily spend for the project: totals and a day-by-day series, optionally split by `provider`, `connection` or `model`. Totals are the sum of the daily rollups. `model` groups use provider-reported breakdowns where a connection has them and itemized events otherwise, so they can sum to less than the day's spend. Defaults to the last 30 days; at most 366.

sincequery · stringoptional

First day, inclusive (rounded down to 00:00 UTC).

untilquery · stringoptional

Last day, exclusive (rounded up to 00:00 UTC). Default: tomorrow.

granularityquery · stringoptional

Bucket size. Only `day` for now.

group_byquery · stringoptional

Split each day by this dimension.

providerquery · stringoptional

Only this provider slug.

connectionquery · stringoptional

Only this connection (`conn_...`).

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, too_many_auth_failures, invalid_parameter

curl -s "https://api.argus.town/v1/spend?group_by=provider" -H "Authorization: Bearer $ARGUS_API_KEY"
# by provider → 200
{
  "object": "spend",
  "since": "2026-09-21T00:00:00.000Z",
  "until": "2026-10-22T00:00:00.000Z",
  "granularity": "day",
  "group_by": "provider",
  "totals": {
    "spend_usd": 18.4,
    "requests": 1200,
    "tokens_input": 900000,
    "tokens_output": 120000,
    "errors": 3
  },
  "series": [
    {
      "period_start": "2026-10-01T00:00:00.000Z",
      "spend_usd": 18.4,
      "requests": 1200,
      "groups": [
        {
          "key": "openai",
          "spend_usd": 18.4,
          "requests": 1200
        }
      ]
    }
  ],
  "next_steps": []
}

Connections

GET/v1/connectionsAPI keyMCP tool connections_list

List connections. Providers the project tracks, polled, pushed or both, newest first. Credentials are never returned.

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

providerquery · stringoptional

Only connections to this provider slug.

statusquery · stringoptional

Only connections in this status.

originquery · stringoptional

Only connections fed this way.

expand[]query · string arrayoptional

Fields to include. One or more of: metrics, advisories.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/connections?limit=10&expand[]=metrics" -H "Authorization: Bearer $ARGUS_API_KEY"
# with metrics → 200
{
  "object": "list",
  "data": [
    {
      "object": "connection",
      "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
      "provider": "openrouter",
      "origin": "poll",
      "label": "OpenRouter (production)",
      "credential_type": "api_key",
      "status": "active",
      "status_detail": null,
      "last_synced_at": "2026-10-01T09:30:00.000Z",
      "created_at": "2026-10-01T09:30:00.000Z",
      "updated_at": "2026-10-01T09:30:00.000Z",
      "metrics": {
        "balance_usd": 42.5,
        "burn_per_day": 3.1,
        "days_to_zero": 13.7,
        "mtd_spend_usd": 18.4,
        "spend_basis": "reported",
        "observed_since": null,
        "balance_scope": "account",
        "projected_month_end_usd": 55.2,
        "wow_change_pct": 4.2,
        "rate_limit_headroom_pct": 80,
        "error_rate_pct": 0.4,
        "accuracy": "exact",
        "cost_drift_pct": 0.3,
        "billing_mode": "usage_drawdown",
        "utilization_pct": null,
        "used_amount": null,
        "included_amount": null,
        "included_meter_id": null,
        "subscription_fee_usd": null,
        "effective_usd_per_unit": null,
        "days_to_reset": null,
        "recommended_action": null,
        "recommended_tier": null,
        "recommended_delta_usd": null
      },
      "metered": true
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/v1/connections/{id}API keyMCP tool connections_get

Get a connection. One connection. Expand `usage`, `breakdown`, `rate_limits`, `sync_runs` and `plan` for the detail view; expansions only add fields.

idpathrequired

Connection id, with or without the `conn_` prefix.

expand[]query · string arrayoptional

Fields to include. One or more of: metrics, advisories, usage, breakdown, rate_limits, sync_runs, plan.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, connection_not_found, invalid_parameter

curl -s "https://api.argus.town/v1/connections/$ID?expand[]=plan" -H "Authorization: Bearer $ARGUS_API_KEY"
# with plan → 200
{
  "object": "connection",
  "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
  "provider": "openrouter",
  "origin": "poll",
  "label": "OpenRouter (production)",
  "credential_type": "api_key",
  "status": "active",
  "status_detail": null,
  "last_synced_at": "2026-10-01T09:30:00.000Z",
  "created_at": "2026-10-01T09:30:00.000Z",
  "updated_at": "2026-10-01T09:30:00.000Z",
  "plan": null
}
POST/v1/connectionsAPI keyMCP tool connections_create

Connect a provider. Poll a provider with a credential. Argus checks the credential with the provider before storing it (encrypted; it is never returned) and queues the first sync. A repeated `label` for the same provider is a 409 `label_conflict`: rotate the credential with `PATCH /v1/connections/{id}` instead. Without a `label` an extra account gets a distinct default one. A provider the project already tracks by telemetry only (no label given) is upgraded in place (`origin: both`). At most 100 polled connections per project (telemetry-only ones are capped separately).

The provider, its credential and optionally a label.

providerstringrequired

Provider slug from `GET /v1/providers?connectable=true`.

credential_typestringoptional

Defaults to the provider's first supported credential type.

secretstringrequired

The provider credential. Checked with the provider, stored encrypted, never returned.

labelstringoptional

a string at most 80 character(s) long

metaobjectoptional

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, invalid_request_body, invalid_parameter, missing_parameter, invalid_idempotency_key, idempotency_key_reused, payload_too_large, provider_not_connectable, credential_invalid, provider_unreachable, label_conflict, quota_exceeded

curl -s -X POST "https://api.argus.town/v1/connections" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"provider":"millionverifier","secret":"mv-example-key","label":"Verifier"}'
# connect → 201
{
  "object": "connection",
  "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
  "provider": "millionverifier",
  "origin": "poll",
  "label": "Verifier",
  "credential_type": "api_key",
  "status": "active",
  "status_detail": null,
  "last_synced_at": null,
  "created_at": "2026-10-01T09:30:00.000Z",
  "updated_at": "2026-10-01T09:30:00.000Z"
}
# repeated label → 409
{
  "error": {
    "type": "conflict",
    "code": "label_conflict",
    "message": "Another millionverifier connection is named \"Verifier\".",
    "param": "label",
    "doc_url": "https://argus.town/docs/errors#label_conflict",
    "request_id": null
  }
}
PATCH/v1/connections/{id}API keyMCP tool connections_update

Update a connection. Relabel a connection or rotate its credential. Send `secret` to rotate: it is checked with the provider, replaces the stored credential and re-queues a sync. `credential_type` and `meta` are part of the credential, so they are accepted only together with `secret` (non-secret fields you leave out of `meta` keep their stored values). A connection an operator suspended stays suspended. Credentials of AI-built connectors rotate in the app. Nothing secret is ever returned.

idpathrequired

Connection id, with or without the `conn_` prefix.

At least one of `label` or `secret`.

labelstringoptional

a string at most 80 character(s) long

secretstringoptional

A replacement credential. Checked with the provider before it is stored. `credential_type` and `meta` change only together with it.

credential_typestringoptional

metaobjectoptional

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, connection_not_found, invalid_request_body, invalid_parameter, invalid_idempotency_key, idempotency_key_reused, payload_too_large, credential_invalid, provider_unreachable, label_conflict

curl -s -X PATCH "https://api.argus.town/v1/connections/$ID" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"label":"OpenAI (production)"}'
# relabel → 200
{
  "object": "connection",
  "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
  "provider": "openai",
  "origin": "poll",
  "label": "OpenAI (production)",
  "credential_type": "api_key",
  "status": "active",
  "status_detail": null,
  "last_synced_at": "2026-10-01T09:30:00.000Z",
  "created_at": "2026-10-01T09:30:00.000Z",
  "updated_at": "2026-10-01T09:30:00.000Z"
}
# meta without secret → 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "\"meta\" can only change together with \"secret\": it is validated and stored with the credential.",
    "param": "meta",
    "doc_url": "https://argus.town/docs/errors#invalid_parameter",
    "request_id": null
  }
}
DELETE/v1/connections/{id}Signed-in app only

Delete a connection. Stop tracking a provider and delete its stored credential and history. Session only: an API key is refused with `session_required`. Unknown ids are a 404.

idpathrequired

Connection id, with or without the `conn_` prefix.

Errors: session_required, connection_not_found

# delete → 204
(no content)
# api key refused → 403
{
  "error": {
    "type": "permission_error",
    "code": "session_required",
    "message": "This action needs a signed-in session in the Argus app. API keys cannot perform it.",
    "param": null,
    "doc_url": "https://argus.town/docs/errors#session_required",
    "next_step": {
      "hint": "Do this from the Argus app, signed in as a member of the project.",
      "href": "https://argus.town/settings"
    },
    "request_id": null
  }
}

Events

POST/v1/eventsSource secret

Send events. Send one event or a batch of up to 1,000. Authenticate with a source secret (`Authorization: Bearer argus_src_...` plus `Argus-Source: <source id>`) or HMAC-sign the body as `POST /ingest` does. A source credential can only write events; it cannot read anything. Argus prices events without a `cost_usd` from `model` and `units`. Bad events are rejected one by one in `rejected`; the rest are queued. Re-sending an event `id` is a no-op, and an `Idempotency-Key` makes the whole request safe to retry for 24 hours. Bearer mode has no replay protection: a captured request can be replayed, so retry with the same `Idempotency-Key` (or give events ids). HMAC mode rejects timestamps more than 5 minutes off, and a replayed signed body without event ids derives the same ids and stores once. After a timeout, resend the identical signed request within 5 minutes or use explicit event ids: a re-signed retry derives new ids and can double-count if the first attempt died before its response was stored. Rate limits per source: 6,000 events and 600 requests a minute. Telemetry may create at most 10 new connections per request and 100 per project. `accepted` counts events queued: the queue consumer prices events without a `cost_usd` afterwards, and drops (and logs) an event whose derived cost exceeds 1,000,000 USD, which cannot be known at request time.

backfillquery · booleanoptional

Allow `occurred_at` up to 29 days back instead of 48 hours.

An event, or `{events: [...], backfill?: true}`. `backfill=true` as a query parameter works too.

Send an Idempotency-Key header to make retries safe.

Errors: missing_source_credentials, invalid_source_credentials, timestamp_stale, key_type_not_allowed, too_many_auth_failures, invalid_request_body, invalid_idempotency_key, payload_too_large, too_many_events, no_valid_events, idempotency_key_reused, invalid_parameter

curl -s -X POST "https://api.argus.town/v1/events" -H "Authorization: Bearer $ARGUS_SOURCE_SECRET" -H "Argus-Source: $ARGUS_SOURCE_ID" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"provider":"openai","model":"gpt-4o","units":{"input_tokens":900,"output_tokens":120},"metadata":{"run":"nightly"}}'
# one event → 202
{
  "object": "event_batch",
  "accepted": 1,
  "rejected": [],
  "rejected_count": 0,
  "warnings": [],
  "warnings_count": 0,
  "events": [
    {
      "id": "evt_6f1d2c4a9b8e47d3a05c1e7b9d2f4a68"
    }
  ],
  "connections": [
    {
      "id": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
      "provider": "openai",
      "created": false
    }
  ],
  "next_steps": [
    {
      "rel": "read_events",
      "method": "GET",
      "href": "/v1/events?id=evt_6f1d2c4a9b8e47d3a05c1e7b9d2f4a68&since=2026-10-01T09%3A29%3A00.000Z&until=2026-10-01T09%3A31%3A00.000Z",
      "why": "Read the event back once the queue has stored it, usually within seconds."
    }
  ]
}
# every event rejected → 422
{
  "error": {
    "type": "invalid_request",
    "code": "no_valid_events",
    "message": "None of the 1 events were valid. First: events[0]: event must be an object.",
    "param": "events[0]",
    "doc_url": "https://argus.town/docs/errors#no_valid_events",
    "next_step": {
      "hint": "Validate the batch to see every rejection.",
      "method": "POST",
      "href": "/v1/events/validate"
    },
    "request_id": null
  }
}
POST/v1/events/validateSource secret

Validate events. Runs the exact pipeline of `POST /v1/events` and stores nothing: which events would be accepted, which rejected and why, and the normalized events. Same credentials and body limits; counts against the same events rate limit.

backfillquery · booleanoptional

Validate as a backfill batch (`occurred_at` up to 29 days back).

The same body as `POST /v1/events`.

Errors: missing_source_credentials, invalid_source_credentials, timestamp_stale, key_type_not_allowed, too_many_auth_failures, invalid_request_body, payload_too_large, too_many_events, invalid_parameter

curl -s -X POST "https://api.argus.town/v1/events/validate" -H "Authorization: Bearer $ARGUS_SOURCE_SECRET" -H "Argus-Source: $ARGUS_SOURCE_ID" -H "content-type: application/json" -d '{"events":[{"provider":"openai","model":"gpt-4o","colour":"red"},{"provider":"openai","status":"ok"}]}'
# one bad, one good → 200
{
  "object": "event_validation",
  "valid": false,
  "accepted": 1,
  "rejected": [
    {
      "index": 1,
      "id": null,
      "code": "invalid_event",
      "message": "status must be a finite number"
    }
  ],
  "rejected_count": 1,
  "warnings": [
    {
      "index": 0,
      "code": "unknown_field",
      "field": "colour",
      "message": "`colour` is not an event field and was ignored."
    }
  ],
  "warnings_count": 1,
  "normalized_events": [
    {
      "provider": "openai",
      "model": "gpt-4o"
    }
  ],
  "next_steps": []
}
GET/v1/eventsAPI keyMCP tool events_list

List events. The project's call ledger, newest first, paged with a keyset cursor so events sharing a timestamp are never skipped. Time-bounded: `since` defaults to 31 days before `until` (default: 48 hours ahead of now) and the window is at most 31 days. `id` and `metadata[key]=value` filters need an explicit `since` and a window of at most 7 days. By default only events with a cost or a status of 400 or above are listed; `view=all` also lists cost-less events, and filtering by `id` or `cost_provenance` always does. Every accepted event counts toward `events_this_month` on `GET /billing`, listed or not.

limitquery · integeroptional

Page size. Default 25, at most 500.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

connectionquery · stringoptional

Only events of this connection (`conn_...`).

providerquery · stringoptional

Only events of this provider slug.

modelquery · stringoptional

Only events of this model.

idquery · stringoptional

Only the event with this id (any source of the project). Needs `since`, window of at most 7 days.

statusquery · integeroptional

Only events with this HTTP status.

cost_provenancequery · stringoptional

Only events whose cost was obtained this way.

sincequery · stringoptional

Oldest `occurred_at`, inclusive.

untilquery · stringoptional

Newest `occurred_at`, exclusive.

viewquery · stringoptional

`all` lists cost-less events with a status below 400 too.

metadataquery · string mapoptional

Filter on metadata, for example `metadata[run]=nightly`. Up to 5; needs `since` (window of at most 7 days).

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, too_many_auth_failures, invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/events?limit=1&view=all" -H "Authorization: Bearer $ARGUS_API_KEY"
# recent → 200
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "01JAX7Q2M5N8R4T6V9W3Y1Z0BC",
      "source": "src_5b1c0e9a",
      "connection": "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11",
      "provider": "openai",
      "hostname": "api.openai.com",
      "endpoint": "/v1/chat/completions",
      "method": "POST",
      "model": "gpt-4o",
      "status": 200,
      "latency_ms": 812,
      "units": {
        "input_tokens": 900,
        "output_tokens": 120
      },
      "metadata": {
        "run": "nightly"
      },
      "cost_usd": 0.00345,
      "cost_micros": 3450,
      "cost_provenance": "derived",
      "price_match": "exact",
      "provider_request_id": null,
      "credits_spent": null,
      "credits_remaining": null,
      "occurred_at": "2026-10-01T09:30:00.000Z",
      "received_at": "2026-10-01T09:30:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Syncs

POST/v1/connections/{id}/syncsAPI keyMCP tool syncs_create

Refresh a connection. Queue an immediate refresh of balance, usage and plan, and get a sync to poll with `GET /v1/syncs/{id}`. At most one refresh is queued per connection a minute, whatever state the last one is in: a request inside that window returns the latest sync instead of queueing another. Telemetry-only and suspended connections have nothing to poll: 409 `connection_not_pollable`.

idpathrequired

Connection id, with or without the `conn_` prefix.

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, connection_not_found, connection_not_pollable, invalid_idempotency_key, idempotency_key_reused

curl -s -X POST "https://api.argus.town/v1/connections/$CONNECTION/syncs" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)"
# queued → 202
{
  "object": "sync",
  "id": "sync_5d0c7a21-8e4b-4f6a-9c13-2b7e1a90d4f8",
  "kind": "refresh",
  "status": "queued",
  "started_at": "2026-10-01T09:30:00.000Z",
  "finished_at": null,
  "records_written": 0,
  "error": null
}
# telemetry only → 409
{
  "error": {
    "type": "conflict",
    "code": "connection_not_pollable",
    "message": "This connection only receives telemetry, so there is nothing to refresh.",
    "param": null,
    "doc_url": "https://argus.town/docs/errors#connection_not_pollable",
    "request_id": null
  }
}
GET/v1/connections/{id}/syncsAPI key

List refreshes. Refreshes requested for the connection, newest first. A sync is `queued`, `running` once some of its runs have reported, then `success`, `partial` or `error`. A transient provider error reads `partial` until the retry lands; a sync the queue never finished is closed after 30 minutes with what it has.

idpathrequired

Connection id, with or without the `conn_` prefix.

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, connection_not_found, invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/connections/$CONNECTION/syncs?limit=5" -H "Authorization: Bearer $ARGUS_API_KEY"
# history → 200
{
  "object": "list",
  "data": [
    {
      "object": "sync",
      "id": "sync_5d0c7a21-8e4b-4f6a-9c13-2b7e1a90d4f8",
      "kind": "refresh",
      "status": "queued",
      "started_at": "2026-10-01T09:30:00.000Z",
      "finished_at": null,
      "records_written": 0,
      "error": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/v1/syncs/{id}API keyMCP tool syncs_get

Get a sync. One refresh, or one scheduled run as listed under a connection's `sync_runs` expansion.

idpathrequired

Sync id, with or without the `sync_` prefix.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, sync_not_found

curl -s "https://api.argus.town/v1/syncs/$SYNC" -H "Authorization: Bearer $ARGUS_API_KEY"
# queued → 200
{
  "object": "sync",
  "id": "sync_5d0c7a21-8e4b-4f6a-9c13-2b7e1a90d4f8",
  "kind": "refresh",
  "status": "queued",
  "started_at": "2026-10-01T09:30:00.000Z",
  "finished_at": null,
  "records_written": 0,
  "error": null
}

Alerts

POST/v1/alert_rulesAPI keyMCP tool alert_rules_create

Create an alert rule. Email the project's members when a connection crosses a threshold. `threshold` is USD, days or percent depending on `type`. `connections` lists the connections to watch; omit it to watch all of them. An id that is not a connection of the project is a 422, never a rule that silently watches everything. At most 100 rules per project.

The condition and what it watches.

type"low_balance" | "days_to_zero" | "spend_spike" | "projected_overage" | "rate_limit_headroom" | "error_rate" | "cost_drift"required

thresholdnumberrequired

USD, days or percent, depending on `type`.

connectionsstring[]optional

Connection ids the rule watches. Omit to watch every connection. An unknown id is an error.

enabledbooleanoptional

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, invalid_request_body, invalid_parameter, missing_parameter, invalid_idempotency_key, idempotency_key_reused, payload_too_large, connection_not_found, quota_exceeded

curl -s -X POST "https://api.argus.town/v1/alert_rules" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"type":"low_balance","threshold":10,"connections":["{id}"]}'
# low balance → 201
{
  "object": "alert_rule",
  "id": "alr_2c8e5b13-7d4a-4f90-b6a1-3e9d0c7f5a82",
  "type": "low_balance",
  "threshold": 10,
  "connections": [
    "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11"
  ],
  "enabled": true,
  "channel": "email",
  "last_fired_at": null,
  "created_at": "2026-10-01T09:30:00.000Z"
}
# unknown connection → 422
{
  "error": {
    "type": "not_found",
    "code": "connection_not_found",
    "message": "No connection has the id conn_00000000-0000-0000-0000-000000000000 in this project.",
    "param": "connections[0]",
    "doc_url": "https://argus.town/docs/errors#connection_not_found",
    "next_step": {
      "hint": "List this project's connections.",
      "method": "GET",
      "href": "/v1/connections"
    },
    "request_id": null
  }
}
GET/v1/alert_rulesAPI keyMCP tool alert_rules_list

List alert rules. The project's alert rules, newest first.

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/alert_rules?limit=10" -H "Authorization: Bearer $ARGUS_API_KEY"
# rules → 200
{
  "object": "list",
  "data": [
    {
      "object": "alert_rule",
      "id": "alr_2c8e5b13-7d4a-4f90-b6a1-3e9d0c7f5a82",
      "type": "low_balance",
      "threshold": 10,
      "connections": [
        "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11"
      ],
      "enabled": true,
      "channel": "email",
      "last_fired_at": null,
      "created_at": "2026-10-01T09:30:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
PATCH/v1/alert_rules/{id}API keyMCP tool alert_rules_update

Update an alert rule. Change any of `type`, `threshold`, `connections` (`null` watches every connection) and `enabled`. Connection ids are checked like on creation.

idpathrequired

Rule id, with or without the `alr_` prefix.

At least one field to change.

type"low_balance" | "days_to_zero" | "spend_spike" | "projected_overage" | "rate_limit_headroom" | "error_rate" | "cost_drift"optional

thresholdnumberoptional

a number between 0 and 1000000000000

connectionsstring[] | nulloptional

`null` makes the rule watch every connection.

enabledbooleanoptional

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, alert_rule_not_found, invalid_request_body, invalid_parameter, invalid_idempotency_key, idempotency_key_reused, payload_too_large, connection_not_found

curl -s -X PATCH "https://api.argus.town/v1/alert_rules/$ALERT_RULE" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"threshold":25}'
# raise the threshold → 200
{
  "object": "alert_rule",
  "id": "alr_2c8e5b13-7d4a-4f90-b6a1-3e9d0c7f5a82",
  "type": "low_balance",
  "threshold": 25,
  "connections": [
    "conn_0b6f2c1e-6c1d-4a39-9f43-5b0d2f7a8c11"
  ],
  "enabled": true,
  "channel": "email",
  "last_fired_at": null,
  "created_at": "2026-10-01T09:30:00.000Z"
}
# empty connections → 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "Invalid value for \"connections\": Expected an array of at least 1 item(s), actual []",
    "param": "connections",
    "doc_url": "https://argus.town/docs/errors#invalid_parameter",
    "request_id": null
  }
}
GET/v1/alert_firingsAPI keyMCP tool alert_firings_list

List alert firings. When rules fired, newest first. Filter with `rule` or `connection` (the same ids the objects carry).

limitquery · integeroptional

Page size. Default 25.

cursorquery · stringoptional

Opaque cursor from a previous page's `next_cursor`.

rulequery · stringoptional

Only firings of this rule (`alr_...`).

connectionquery · stringoptional

Only firings for this connection (`conn_...`).

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, invalid_parameter, invalid_cursor

curl -s "https://api.argus.town/v1/alert_firings?limit=10" -H "Authorization: Bearer $ARGUS_API_KEY"
# none yet → 200
{
  "object": "list",
  "data": [],
  "has_more": false,
  "next_cursor": null
}
DELETE/v1/alert_rules/{id}Signed-in app only

Delete an alert rule. Session only: an API key is refused with `session_required`. Unknown ids are a 404.

idpathrequired

Rule id, with or without the `alr_` prefix.

Errors: session_required, alert_rule_not_found

# delete → 204
(no content)
# api key refused → 403
{
  "error": {
    "type": "permission_error",
    "code": "session_required",
    "message": "This action needs a signed-in session in the Argus app. API keys cannot perform it.",
    "param": null,
    "doc_url": "https://argus.town/docs/errors#session_required",
    "next_step": {
      "hint": "Do this from the Argus app, signed in as a member of the project.",
      "href": "https://argus.town/settings"
    },
    "request_id": null
  }
}

Negotiated pricing

PUT/v1/negotiated_pricing/{provider}API keyMCP tool negotiated_pricing_put

Set negotiated pricing. Record what your deal with a provider costs, so spend is priced at your rates instead of list price. `rates` are set one per `sku` and `unit` (rates you do not list are kept); `plan` replaces the negotiated subscription, and fields you omit from it are cleared. Every entry is validated and one bad entry rejects the request with `param` naming it: nothing is skipped silently. Events from the last 7 days that were left unpriced are repriced. `provider` is any provider slug your events use, catalog or not. At most 500 rates and 100 plans per project. List rates are read-only (`GET /v1/negotiated_pricing/{provider}?expand[]=list_rates`); resetting to list price is done in the app.

providerpathrequired

Provider slug, for example `openrouter`.

Rates, a plan, or both.

ratesobject[]optional

Rates to set, one per `sku` and `unit`; rates already set and not listed are kept. One bad entry rejects the whole request.

planobjectoptional

The negotiated subscription. Fields you omit are cleared.

Send an Idempotency-Key header to make retries safe.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, provider_not_found, invalid_request_body, invalid_parameter, invalid_idempotency_key, idempotency_key_reused, payload_too_large, quota_exceeded

curl -s -X PUT "https://api.argus.town/v1/negotiated_pricing/openrouter" -H "Authorization: Bearer $ARGUS_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "content-type: application/json" -d '{"rates":[{"sku":"gpt-4o","unit":"input_tokens","nano_usd_per_unit":2000}]}'
# one rate → 200
{
  "object": "negotiated_pricing",
  "provider": "openrouter",
  "rates": [
    {
      "sku": "gpt-4o",
      "unit": "input_tokens",
      "nano_usd_per_unit": 2000
    }
  ],
  "plan": null,
  "updated_at": "2026-10-01T09:30:00.000Z"
}
# malformed rate → 400
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "Invalid value for \"rates[0].nano_usd_per_unit\": Expected a number between 0 and 1000000000000, actual -1",
    "param": "rates[0].nano_usd_per_unit",
    "doc_url": "https://argus.town/docs/errors#invalid_parameter",
    "request_id": null
  }
}
GET/v1/negotiated_pricing/{provider}API keyMCP tool negotiated_pricing_get

Get negotiated pricing. The project's own rates and subscription for a provider; empty `rates` and a null `plan` when nothing is negotiated. `expand[]=list_rates` adds Argus's list rates, which negotiated rates replace one for one.

providerpathrequired

Provider slug, for example `openrouter`.

expand[]query · string arrayoptional

Fields to include. One or more of: list_rates.

Errors: missing_credentials, invalid_api_key, key_type_not_allowed, provider_not_found, invalid_parameter

curl -s "https://api.argus.town/v1/negotiated_pricing/openrouter" -H "Authorization: Bearer $ARGUS_API_KEY"
# negotiated → 200
{
  "object": "negotiated_pricing",
  "provider": "openrouter",
  "rates": [
    {
      "sku": "gpt-4o",
      "unit": "input_tokens",
      "nano_usd_per_unit": 2000
    }
  ],
  "plan": null,
  "updated_at": "2026-10-01T09:30:00.000Z"
}
DELETE/v1/negotiated_pricing/{provider}Signed-in app only

Reset to list pricing. Remove every negotiated rate and the negotiated plan for a provider, or with `sku` and `unit` just that one rate (the plan stays). Session only: an API key is refused with `session_required`. A project with nothing negotiated, or no rate for that `sku` and `unit`, gets a 404.

providerpathrequired

Provider slug, for example `openrouter`.

skuquery · stringoptional

Reset only this rate; needs `unit`.

unitquery · stringoptional

The unit of the rate to reset; needs `sku`.

Errors: session_required, provider_not_found, negotiated_pricing_not_found, invalid_parameter

# reset → 204
(no content)
# api key refused → 403
{
  "error": {
    "type": "permission_error",
    "code": "session_required",
    "message": "This action needs a signed-in session in the Argus app. API keys cannot perform it.",
    "param": null,
    "doc_url": "https://argus.town/docs/errors#session_required",
    "next_step": {
      "hint": "Do this from the Argus app, signed in as a member of the project.",
      "href": "https://argus.town/settings"
    },
    "request_id": null
  }
}