Docs
Errors

Report errors from your backend

The same error pipeline that powers the JS recorder accepts errors from any backend or native client. Authenticate with your site's server API key, POST a small JSON payload, and the error shows up in your dashboard alongside browser errors — tagged backend so you can filter frontend and server-side issues separately.

Endpoint

POST https://api.nevision.app/v1/ingest/errors
Content-Type: application/json
x-nevision-api-key: sk_YOUR_SERVER_KEY

Find siteId and the Server API Key (sk_…) in your dashboard under Sites → [your site] → Install. It is the same key you use for backend logs. Keep it on your servers — never embed it in code that ships to a browser or an app.

Native and mobile apps can't keep a secret, so they send the public site key (nv_…) instead. That key is already in every page that loads the recorder, so treat errors it reports as client-side, not as proof they came from your servers.

Payload

{
  "siteId": "YOUR_SITE_ID",
  "errors": [
    {
      "type": "uncaught",
      "message": "NullPointerException in /checkout",
      "stack": "...",
      "fingerprint": "checkout-npe-v1",
      "timestamp": 1719000000000,
      "url": "https://api.example.com/checkout",
      "filename": "CheckoutController.java",
      "lineno": 142
    }
  ]
}

fingerprint is how errors get grouped — pick a stable value (exception class + route works well). timestamp is Unix epoch milliseconds. Up to 20 errors per request.

Python

import time, requests

def report_error(exc: Exception, route: str):
    requests.post(
        "https://api.nevision.app/v1/ingest/errors",
        headers={"x-nevision-api-key": "sk_YOUR_SERVER_KEY"},
        json={
            "siteId": "YOUR_SITE_ID",
            "errors": [{
                "type": "uncaught",
                "message": str(exc),
                "stack": "".join(__import__("traceback").format_exception(exc)),
                "fingerprint": f"{type(exc).__name__}:{route}",
                "timestamp": int(time.time() * 1000),
                "url": route,
            }],
        },
        timeout=5,
    )

try:
    do_work()
except Exception as e:
    report_error(e, "/checkout")
    raise

PHP

<?php
function nevision_report(Throwable $e, string $route): void {
    $payload = json_encode([
        "siteId" => "YOUR_SITE_ID",
        "errors" => [[
            "type" => "uncaught",
            "message" => $e->getMessage(),
            "stack" => $e->getTraceAsString(),
            "fingerprint" => get_class($e) . ":" . $route,
            "timestamp" => (int) (microtime(true) * 1000),
            "url" => $route,
            "filename" => $e->getFile(),
            "lineno" => $e->getLine(),
        ]],
    ]);

    $ch = curl_init("https://api.nevision.app/v1/ingest/errors");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json",
            "x-nevision-api-key: sk_YOUR_SERVER_KEY",
        ],
        CURLOPT_TIMEOUT => 5,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    curl_exec($ch);
    curl_close($ch);
}

.NET (C#)

using System.Net.Http;
using System.Net.Http.Json;

public static class Nevision
{
    private static readonly HttpClient Http = new() {
        BaseAddress = new Uri("https://api.nevision.app/")
    };

    public static async Task ReportAsync(Exception ex, string route)
    {
        var req = new HttpRequestMessage(HttpMethod.Post, "public/errors/ingest")
        {
            Content = JsonContent.Create(new {
                siteId = "YOUR_SITE_ID",
                errors = new[] {
                    new {
                        type = "uncaught",
                        message = ex.Message,
                        stack = ex.ToString(),
                        fingerprint = $"{ex.GetType().Name}:{route}",
                        timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
                        url = route,
                    }
                }
            })
        };
        req.Headers.Add("x-nevision-api-key", "sk_YOUR_SERVER_KEY");
        await Http.SendAsync(req);
    }
}

Java

import java.net.URI;
import java.net.http.*;
import java.io.StringWriter;
import java.io.PrintWriter;

public class Nevision {
    private static final HttpClient CLIENT = HttpClient.newHttpClient();

    public static void report(Throwable t, String route) throws Exception {
        StringWriter sw = new StringWriter();
        t.printStackTrace(new PrintWriter(sw));
        String fingerprint = t.getClass().getSimpleName() + ":" + route;

        String body = """
            {"siteId":"YOUR_SITE_ID","errors":[{
              "type":"uncaught",
              "message":%s,
              "stack":%s,
              "fingerprint":%s,
              "timestamp":%d,
              "url":%s
            }]}
            """.formatted(
                quote(t.getMessage()),
                quote(sw.toString()),
                quote(fingerprint),
                System.currentTimeMillis(),
                quote(route)
            );

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.nevision.app/v1/ingest/errors"))
            .header("Content-Type", "application/json")
            .header("x-nevision-api-key", "sk_YOUR_SERVER_KEY")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        CLIENT.sendAsync(req, HttpResponse.BodyHandlers.discarding());
    }

    private static String quote(String s) {
        if (s == null) return "null";
        return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
    }
}

Kotlin (Android / JVM)

import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject

object Nevision {
    private val client = OkHttpClient()
    private val JSON = "application/json".toMediaType()

    fun report(t: Throwable, route: String) {
        val payload = JSONObject().apply {
            put("siteId", "YOUR_SITE_ID")
            put("errors", JSONArray().put(JSONObject().apply {
                put("type", "uncaught")
                put("message", t.message ?: t.javaClass.simpleName)
                put("stack", t.stackTraceToString())
                put("fingerprint", "${t.javaClass.simpleName}:$route")
                put("timestamp", System.currentTimeMillis())
                put("url", route)
            }))
        }

        val req = Request.Builder()
            .url("https://api.nevision.app/v1/ingest/errors")
            .header("x-nevision-api-key", "sk_YOUR_SERVER_KEY")
            .post(payload.toString().toRequestBody(JSON))
            .build()

        client.newCall(req).enqueue(object : Callback {
            override fun onFailure(call: Call, e: java.io.IOException) {}
            override fun onResponse(call: Call, response: Response) { response.close() }
        })
    }
}

iOS (Swift)

import Foundation

enum Nevision {
    static func report(_ error: Error, route: String) {
        let url = URL(string: "https://api.nevision.app/v1/ingest/errors")!
        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("nv_YOUR_SITE_KEY", forHTTPHeaderField: "x-nevision-api-key")

        let ns = error as NSError
        let payload: [String: Any] = [
            "siteId": "YOUR_SITE_ID",
            "errors": [[
                "type": "uncaught",
                "message": error.localizedDescription,
                "stack": "\(ns.domain) \(ns.code)\n\(Thread.callStackSymbols.joined(separator: \"\\n\"))",
                "fingerprint": "\(ns.domain):\(route)",
                "timestamp": Int(Date().timeIntervalSince1970 * 1000),
                "url": route,
            ]],
        ]
        req.httpBody = try? JSONSerialization.data(withJSONObject: payload)

        URLSession.shared.dataTask(with: req).resume()
    }
}

Response

{ "accepted": 1 }

The endpoint always returns 200 — check the accepted count to verify ingestion. If it's 0, the response includes an error field (Invalid site, Errors disabled, or limitReached: true when you've hit your monthly plan quota).

Tips

  • Pick stable fingerprints. ExceptionClass:route groups well; including the message tends to over-split.
  • Fire and forget. Don't block your request path waiting for the response — use a short timeout (5s) or a background queue.
  • Batch when you can. Up to 20 errors per request reduces overhead in worker/cron contexts.
  • Don't ship the server key. sk_… stays on servers; browsers and apps only ever see the nv_… site key.