API-only error monitoring: no session replay, no tracing, no agent
What a small US or EU team gives up by dropping replay and tracing, what a metered exception store costs instead, and a Python 3 integration you can finish today.
If your product is a backend API, most of an error-monitoring bill buys features that only make sense for a browser app. Session replay records users you don’t have. Distributed tracing answers questions a four-service startup can already answer by reading a log. Infrai’s errors namespace deliberately ships none of that: nine REST routes that store an exception, group it by fingerprint, let you search it and let you close it — priced per captured event, with every read free.
That’s a narrower product than Sentry or Raygun, and narrower is the point of this page. Below is what you’re giving up (in a table, so you can decide honestly), what the remaining surface costs, and a Python 3 integration that a small team can land between lunch and standup.
The four things an API-only store won’t do for you
| Feature | What it’s actually for | Available here | Buy it from |
|---|---|---|---|
| Session replay | Reproducing a browser bug from the user’s side | No | Sentry, Raygun |
| Distributed tracing / APM | Finding which of 40 services burned 900ms | No | Datadog |
| Source-map symbolication | Turning minified frames back into your code | No | Sentry, Bugsnag |
| Built-in alert rules and on-call | Paging a human without you writing a poller | No | Honeybadger, Sentry |
Three of those four are irrelevant to a service whose only client is another service. The fourth is a real gap, and it costs you an afternoon: the read routes are free, so a cron job that queries them and posts to Slack is the standard workaround — we wrote that one up separately at https://docs.infrai.cc/en/guides/errors/answers/best-simple-error-alerting-api-for-nodejs-saas-2025-pol/.
Everything else you’d reach for during an incident is here: capture, event lookup, aggregated groups, full-text search over messages, and resolve.
Reporting from Python
import os
import sys
import traceback
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
RELEASE = os.environ.get("APP_RELEASE", "dev")
ENVIRONMENT = os.environ.get("APP_ENV", "development")
def report(exc: BaseException, scope: str = "app") -> str | None:
"""Send one exception. Returns the group id, or None if reporting failed."""
trace = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
payload = {
"message": trace[:8000],
"exception": type(exc).__name__,
"fingerprint": f"{scope}:{type(exc).__name__}",
"environment": ENVIRONMENT,
"release": RELEASE,
}
try:
res = requests.post(
f"{API}/v1/errors/capture",
headers={"Authorization": f"Bearer {KEY}"},
json=payload,
timeout=3,
)
res.raise_for_status()
return res.json()["data"]["error_group_id"]
except requests.RequestException as send_error:
print(f"error reporting failed: {send_error}", file=sys.stderr)
return None
def excepthook(kind, value, tb):
report(value, scope="uncaught")
sys.__excepthook__(kind, value, tb)
sys.excepthook = excepthook
if __name__ == "__main__":
try:
raise ValueError("ledger reconcile: unexpected currency GBP")
except ValueError as err:
print(report(err, scope="jobs:reconcile"))
The fingerprint is yours to choose and it’s the single most consequential field in the whole payload. Pick something stable — a scope plus the exception class, as above — and one bug stays one group across restarts, hosts and deploys. Put a request id or an order number in there and you’ll wake up to nine thousand groups describing one broken code path.
The timeout matters too. Three seconds is generous for a call that shouldn’t block a request path at all; in a web handler, hand it to a thread or a queue rather than making your user wait on your monitoring.
What the wire actually looks like
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST "https://api.infrai.cc/v1/errors/capture" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"message": "ValueError: ledger reconcile: unexpected currency GBP",
"exception": "ValueError",
"fingerprint": "jobs:reconcile:ValueError",
"environment": "production",
"release": "2026.07.6"
}'
{
"ok": true,
"data": {
"event_id": "evt_err_TX7fUCRNYrv7gkDJ43qs2cBq",
"fingerprint": "475fff230418889b4c16cd43d88ed18ad149ea4bfce48695cc2d4bb083445450",
"error_group_id": "errgrp_IdEyj3ZJ0mb4fZCliXjJ1ENm",
"is_new_group": true,
"dashboard_url": "https://infrai.cc/projects/proj_local/errors/groups/475fff230418889b4c16cd43"
}
}
One caveat we checked against the live API rather than the reference: the exception you send is not parsed into frames. The stored event carries {"type": "Message", "value": "<your message>", "stacktrace": []}, so the traceback survives only as text inside message. Send the whole formatted traceback — as the Python helper above does — because that string is the only trace you’ll get back.
Reading a group later needs no SDK either:
curl -sS "https://api.infrai.cc/v1/errors/group_detail/errgrp_DCZn637btNBwADmDraeaDVCw" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That returns count, first_seen_at, last_seen_at, is_resolved, plus distributions by tag, release and environment — enough to answer “how often, since when, which deploy” without opening a dashboard.
Full-text search over stored messages is the other read you’ll use daily, and a function name lifted from a traceback makes a workable query:
curl -sS "https://api.infrai.cc/v1/errors/search?q=reconcile&limit=5" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The same check from Python, for a post-deploy smoke script that fails your pipeline when a known-bad signature reappears:
import os
import sys
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
res = requests.get(
f"{API}/v1/errors/search",
headers={"Authorization": f"Bearer {KEY}"},
params={"q": "reconcile", "limit": 20},
timeout=5,
)
res.raise_for_status()
items = res.json()["data"]["items"]
recent = [e for e in items if e["release"] == os.environ.get("APP_RELEASE", "dev")]
print(f"{len(recent)} matching events on this release")
if recent:
sys.exit(1)
Search takes q and nothing else — passing environment or level alongside it is accepted and then ignored, so filter the results in your own code as above. An empty q is the one input it rejects outright, with a 400 and INVALID_FILTER_SYNTAX.
The pricing shape, not just the price
Capture is billed per event at $0.00005, verified 2026-07-26, and reads don’t bill at all. New accounts get $2 of free credit, which works out around 39,999 captured events before the first invoice. Check today’s number instead of trusting the paragraph:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '.capabilities[] | select(.id == "errors.capture") | .billing'
Prices in this catalogue move down over time and campaigns run, so the live figure may well be lower than what’s printed here. The shape is what survives: metered per event, no seats, no retention tiers, and no plan ceiling that stops accepting your events three days before renewal. Quota-priced competitors have the opposite profile — generous free tiers that suit a side project, then a cliff exactly when a bad deploy triples your event volume. Neither model is universally better, and if your traffic is small and steady a free tier probably wins on price.
The durable argument isn’t the rate. It’s that the same key already reaches queues, cron, object storage, email and AI inference, so the follow-on work after “record the exception” — retry the job, mail the customer, attribute the cost to a tenant — doesn’t add a vendor, an SDK or an invoice.
US, EU and the residency question
The errors capability is served from both western and China regions, and the endpoint is the same api.infrai.cc host wherever you call from. What the errors routes don’t expose is a per-request region pin or a documented single-region store, so if your data protection agreement requires a named EU-only location in writing, that’s a limitation you should resolve before migrating: Sentry publishes an EU data region, and self-hosted GlitchTip keeps everything on your own infrastructure.
Keep personal data out of message regardless of region — it’s the cheapest compliance decision available, and it’s free.
Who should not do this
If you ship a React frontend and your hardest bugs are minified browser stack traces, you’d be better off paying Sentry; source maps are most of what you’re buying and there’s no counterpart here. Teams running dozens of services with latency budgets want tracing, which means Datadog or an OpenTelemetry backend. And anyone who needs an out-of-the-box on-call rotation should look at Honeybadger before building one.
For a five-service backend where errors are server-side, tracebacks are already readable, and nobody wants a fourth monitoring account — an API-only store is the proportionate answer.