My App

Sending reports via the API

Submit bug reports, feature requests, and suggestions directly to the Bughorn HTTP API — no SDK required.

Every Bughorn SDK is a thin wrapper around a single HTTP endpoint. If there's no SDK for your platform — or you'd rather not add a dependency — you can send reports yourself with any HTTP client.

The endpoint

POST https://bughorn.com/api/v1/submit

That's the production base URL. For local development, use http://localhost:3000 instead.

Authentication

Authenticate with a project API key, sent as a Bearer token:

Authorization: Bearer bh_xxxxxxxxxxxxxxxxxxxxxxxx

You can find (and rotate) a project's key under Project → Settings → API keys.

Bughorn API keys are publishable — they're designed to ship inside client apps (mobile, web, desktop). A key only ever lets a caller create reports for its own project; it grants no read access and can be rotated or revoked at any time. The report's project is derived entirely from the key, so there is no projectId field to set (and any attempt to send one is rejected).

Quick start

Grab your project API key

Copy the key from Project → Settings → API keys. It starts with bh_.

Send a report

curl -X POST https://bughorn.com/api/v1/submit \
  -H "Authorization: Bearer bh_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bug",
    "title": "Crash on checkout",
    "message": "Tapping \"Pay\" on the cart screen crashes the app.",
    "end_user_email": "[email protected]",
    "context": { "app_version": "1.4.2", "os": "iOS 18.1" }
  }'
await fetch("https://bughorn.com/api/v1/submit", {
  method: "POST",
  headers: {
    "Authorization": "Bearer bh_xxxxxxxxxxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "bug",
    title: "Crash on checkout",
    message: 'Tapping "Pay" on the cart screen crashes the app.',
    end_user_email: "[email protected]",
    context: { app_version: "1.4.2", os: "iOS 18.1" },
  }),
});
let url = URL(string: "https://bughorn.com/api/v1/submit")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer bh_xxxxxxxxxxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: [
    "type": "bug",
    "title": "Crash on checkout",
    "message": "Tapping \"Pay\" on the cart screen crashes the app.",
    "end_user_email": "[email protected]",
    "context": ["app_version": "1.4.2", "os": "iOS 18.1"],
])

let (_, response) = try await URLSession.shared.data(for: request)
import requests

requests.post(
    "https://bughorn.com/api/v1/submit",
    headers={"Authorization": "Bearer bh_xxxxxxxxxxxxxxxxxxxxxxxx"},
    json={
        "type": "bug",
        "title": "Crash on checkout",
        "message": 'Tapping "Pay" on the cart screen crashes the app.',
        "end_user_email": "[email protected]",
        "context": {"app_version": "1.4.2", "os": "iOS 18.1"},
    },
)

Check the response

A successful request returns 201 Created:

{ "id": 1234, "status": "received" }

Request body

The body must be JSON. Unknown fields are rejected, so only send the fields below.

FieldTypeRequiredNotes
type"bug" | "feature_request" | "suggestion"yesThe kind of report.
titlestring (≤ 200)conditionalShort summary. Provide title and/or message.
messagestring (≤ 5000)conditionalBody text. Provide title and/or message.
end_user_idstring (≤ 255)noYour identifier for the reporting user.
end_user_emailstring, email (≤ 320)noReporter's email.
end_user_namestring (≤ 255)noReporter's name.
contextobject (≤ 16 KB serialized)noDiagnostic metadata: app version, OS, device, locale, etc.
custom_dataobject (≤ 16 KB serialized)noAny extra structured data you want to attach.
attachmentsarray of Attachment (≤ 10)noLinks to screenshots, logs, etc.

Field rules

  • You must include at least one of title or message.
  • context and custom_data are free-form JSON objects. Use context for diagnostic/environment data and custom_data for your own domain fields.

Attachment

FieldTypeRequiredNotes
urlstring, URL (≤ 2048)yesPublicly reachable file URL.
file_namestring (≤ 255)noDisplay name.
content_typestring (≤ 255)noMIME type, e.g. image/png.
size_bytesinteger ≥ 0noFile size in bytes.

Responses

StatusMeaning
201Report received. Body: { "id": number, "status": "received" }.
400Body was not valid JSON.
401API key missing, malformed, unknown, or revoked.
422Validation failed. Body includes an issues array (see below).
429Rate limit exceeded. Retry after the Retry-After header.

A 422 response lists what went wrong:

{
  "error": "Validation failed",
  "issues": [{ "path": "message", "message": "message or title is required" }]
}

Rate limits

The endpoint is rate limited per API key and per IP address using a fixed one-minute window:

  • 60 requests/minute per API key
  • 120 requests/minute per IP address

Every response includes the current limit state:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 37

When you exceed a limit you get 429 Too Many Requests with a Retry-After header (seconds). Back off until the window resets.

Don't retry 4xx responses (except 429) — they mean the request itself needs fixing. Only retry 429 and 5xx, and respect Retry-After.

CORS

The endpoint allows cross-origin requests from any origin, so you can call it directly from browser JavaScript. Because keys are publishable and requests don't use cookies, this is safe — but treat the key as a public identifier, not a secret.

On this page