Developers

API Reference

v1

Read-only HTTP API to export every report in a project: comments, screenshots, session replay, files, browser context, and conversation.

Field names, endpoints, and status values stay as in the API (e.g. aperto). This page is public; API keys and player tokens are not.

Last updated 3 October 2026

Overview

FixNoty API v1 is a versioned, read-only HTTP interface for exporting every report that belongs to a single project. It is designed for scripts, CI, internal tools, and later automations — including an AI agent that needs the same solving context a human sees in the dashboard.

The API never changes status, never writes comments, never deletes objects, and never returns raw storage paths or secret material. Media is delivered as time-bounded signed URLs. Session replay is delivered either as rrweb event chunks or as a 24-hour signed link to the same interactive player used in the dashboard Replay tab — not as a newly encoded MP4.

  • Protocol: HTTPS JSON. Request bodies are not used (all methods are GET).
  • Prefix: /api/v1. Locale prefixes such as /en or /it are for the website only; they are not part of the API path.
  • Scope: one project per key. Cross-project IDs return 404 with code not_found.
  • Compatibility: status, priority, and category enums are the Italian database strings (for example aperto). Do not translate them in clients.

Base URL and versioning

Production host: https://www.fixnoty.com. Replace with your deployment origin in development. Concatenate the prefix without a trailing slash:

http

https://www.fixnoty.com/api/v1/feedback

Breaking changes ship under a new prefix (/api/v2). Additive JSON fields may appear in v1 without a version bump; clients must ignore unknown keys. There is no OpenAPI document in v1.

Authentication

Authenticate with a project API key. The key is scoped to exactly one project and grants read access only. It authenticates the project, not a user: anyone who holds the secret (a script, a collaborator, an integration) can read every report in that project.

Send the secret with exactly one of these headers:

http

Authorization: Bearer fn_live_…
X-Api-Key: fn_live_…

If both are present, Authorization: Bearer wins. Query-string keys, cookies, and request bodies are ignored.

FieldTypeDescription
Formatstringfn_live_ + 64 lowercase hex characters in production, or fn_test_ + 64 hex in development. Regex: ^fn_(live|test)_[a-f0-9]{64}$.
Storagesha256Only the SHA-256 digest of the secret is stored. The plaintext is shown once in Project settings → API and cannot be recovered.
PrefixstringThe dashboard lists fn_live_ / fn_test_ plus the first 8 hex characters so you can recognize a key without the secret.
LifecycleownerAPI keys are included in the Agency plan (and demo accounts). Trial and Pro cannot create keys; existing keys return 403 plan_required. Only the project owner can create or revoke keys. At most 20 active keys per project. Revocation is immediate: subsequent requests return 401 invalid_api_key, and previously minted 24h replay player links bound to that key stop working.
last_used_atdatetimeUpdated at most once per minute after a successful request. A missing value means the key has never been used successfully.
Treat the secret like a password. Do not commit it, paste it into chat, or put it in client-side JavaScript. Rotate by creating a new key and revoking the old one.

Errors

Failed responses use a stable JSON body. code is always English and is the field you should switch on. error is a human message and may be localized from Accept-Language (it or fallback en).

json

{
  "error": "Invalid or revoked API key.",
  "code": "invalid_api_key"
}
FieldTypeDescription
401 missing_api_keyauthNeither Authorization: Bearer nor X-Api-Key was sent.
401 invalid_api_keyauthMalformed, unknown, or revoked key. Same code in all three cases so attackers cannot probe key existence.
400 invalid_requestqueryIllegal query (status, feedback_type, updated_since, limit, or cursor).
404 not_foundscopeThe project or report does not exist, the id is not a UUID, or it belongs to another project. Cross-project probes are indistinguishable from missing rows.
429 rate_limitedthrottleToo many requests. Retry-After is set to seconds until the sliding window has capacity.
403 plan_requiredplanThe project owner's plan does not include the API. Agency (and demo) does; trial and Pro do not.
503 database_unavailableinfraThe API cannot reach the configured database.

A malformed replay-player token, an expired token, or a token whose API key was revoked is not a v1 error: the watch page and its event stream respond with 404 or 410 and are not part of the versioned contract.

Rate limits

Sliding 60-second windows, counted in process memory per server instance (MVP; not a global Redis quota). Both limits apply:

  • 60 requests / minute / client IP, evaluated before the key is parsed. Unauthenticated callers still consume this budget.
  • 120 requests / minute / authenticated key, evaluated after a successful key lookup.

Exceeding either limit returns 429 with code rate_limited and a Retry-After header (seconds, minimum 1). API access is included with Agency, not sold as a separate plan.

Headers and caching

Every v1 JSON response includes:

http

Cache-Control: private, no-store

Signed media URLs and player links must not be cached by CDNs or shared HTTP caches. Clients may keep a response in local memory until the enclosed expires_at and must call GET /feedback/:id/assets (or /replay) to mint a fresh URL.

Secrets are never written to application logs. Do not log Authorization, X-Api-Key, player tokens, or signed URL query strings.

Cursor pagination

GET /feedback uses keyset pagination on (updated_at DESC, id DESC). It does not use SQL OFFSET.

  • Default page size 50; maximum 100. Values above the maximum are clamped, not rejected.
  • When another page exists, has_more is true and next_cursor is an opaque base64url blob. Pass it back as cursor.
  • A malformed cursor yields 400 invalid_request. Do not invent cursors; they are not a public query language.

Signed assets

Binary objects are never inlined in JSON and the API never returns a project zip. Each file is an object:

json

{
  "url": "https://…/object/sign/…?token=…",
  "expires_in": 1800,
  "expires_at": "2026-10-02T10:30:00.000Z"
}
FieldTypeDescription
screenshot, widget attachments, debug attachmentsTTL 1800s30 minutes from the moment the response is minted.
rrweb manifest + chunks, 24h player watch URLTTL 86400s24 hours. Reuses objects already stored for the dashboard player; no extra encode step.

GET the url directly. After expiry the object store rejects the token; call assets or replay again. Attachment rows also include id, file_name, mime_type, file_size, and created_at.

Session replay

Native capture is rrweb DOM events, not a screen recording. v1 exposes two first-class formats plus optional legacy video:

  1. formats.player — a signed HTML page that mounts the same rrweb player as the dashboard Replay tab. Valid 24 hours. Zero extra storage.
  2. formats.rrweb — signed JSON manifest + ordered chunk files (24 hours). Concatenate chunk event arrays in order and feed them to any rrweb player.
  3. formats.video — present only when a historical MP4 already exists on the row. v1 does not transcode rrweb to MP4. Typical status: not_generated.

formats.still is the report screenshot (30-minute TTL). timeline is the widget action log (clicks, navigation, masked input, JS errors) — a separate, smaller JSON array from rrweb.

Reports without a recording still return HTTP 200 with has_replay: false. HTTP 404 means the report is not in the key’s project.

24-hour player security

The watch URL is a capability link, not a public catalog page. It is minted only after a valid project API key authenticates, bound to that key’s id, and cryptographically signed. Documentation of this page is public; the tokens themselves are secrets.
  • Issued exclusively by authenticated GET /api/v1/feedback/:id and …/replay for a report in the same project as the key.
  • HMAC-SHA256 over an expiry-bearing payload. Tokens are opaque; clients must not parse them.
  • Claims include project id, report id, API key id, and Unix expiry. Revoking the key invalidates every outstanding player URL that was minted with it, even before the 24-hour TTL.
  • Expired, forged, or revoked tokens → 404/410. The watch page does not require a dashboard login (so a browser or a browser-capable agent can open it). Possession of the URL is the credential until expiry or revocation.
  • Response headers: Cache-Control: private, no-store, Referrer-Policy: no-referrer, X-Frame-Options: DENY, Content-Security-Policy: frame-ancestors 'none', X-Robots-Tag: noindex, nofollow, X-Content-Type-Options: nosniff. The player cannot be embedded in a third-party iframe and is not offered to search engines.

The HTML player loads events from an internal GET /api/replay-watch/:token stream (IP-throttled, same security headers). That route is not part of the v1 contract; integrate against formats.player.watch.url or formats.rrweb.

GET /api/v1/project

GET/api/v1/project

Confirms the key and returns the project it is bound to. Use this as a connectivity check before listing reports.

json

{
  "project": {
    "id": "11111111-2222-4333-8444-555555555555",
    "name": "Client site",
    "site_url": "https://staging.example.com"
  }
}
FieldTypeDescription
project.iduuidProject bound to the key.
project.namestringDisplay name.
project.site_urlstringConfigured site URL.

GET /api/v1/feedback

GET/api/v1/feedback

Lightweight index of reports in the project. It never includes recording_events, console_logs, conversation, activity, or signed file URLs. Use detail / assets / replay for those.

Query parameters

FieldTypeDescription
statusstringDefault open. open or aperte = aperto + in_lavorazione + in_attesa_cliente. all or tutti = every status. Otherwise a comma-separated list of database statuses.
feedback_typeenumvisual | debug_replay | text_content | request
updated_sinceISO 8601Inclusive lower bound on updated_at. Invalid timestamps → 400.
limitint1–100, default 50. Non-integers and values < 1 → 400.
cursoropaquenext_cursor from the previous page.
includeignoredParsed but heavy JSONB (recording_events) is never dumped on the list endpoint.

json

{
  "data": [
    {
      "id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
      "pin_number": 4,
      "status": "aperto",
      "priority": "bloccante",
      "category": "bug",
      "feedback_type": "debug_replay",
      "title": "Checkout does not submit the order",
      "comment": "I click Pay and nothing happens.",
      "client_title": null,
      "page_url": "https://staging.example.com/checkout",
      "page_path": "/checkout",
      "created_at": "2026-10-01T14:12:00.000Z",
      "updated_at": "2026-10-01T15:40:00.000Z",
      "last_activity_at": "2026-10-01T15:40:00.000Z",
      "author_name": "Anna",
      "assigned_to": null,
      "has_screenshot": true,
      "has_replay": true,
      "has_attachments": true,
      "has_console_errors": true,
      "attachment_count": 1
    }
  ],
  "next_cursor": null,
  "has_more": false
}
FieldTypeDescription
iduuidReport id.
pin_numbernumberPin shown on the page.
statusenumaperto | in_lavorazione | in_attesa_cliente | risolto | archiviato
priorityenumnormale | importante | bloccante
categoryenumtesto | design | immagine | bug | mobile | contenuto | altro
feedback_typeenumvisual | debug_replay | text_content | request
titlestring | nullDashboard title.
commentstringClient comment.
client_titlestring | nullTitle of a “new request” brief.
page_urlstringFull page URL at capture.
page_pathstringPathname derived from page_url (query and hash stripped).
created_at / updated_atdatetimeUTC ISO 8601. updated_at drives the cursor.
last_activity_atdatetime | nullLast dashboard/portal activity.
author_namestringClient display name.
assigned_toobject | null{ user_id, name, email } when assigned.
has_screenshot / has_replay / has_attachments / has_console_errorsbooleanCheap flags for routing to detail/replay.
attachment_countnumberWidget attachment count (not debug files).
next_cursorstring | nullOpaque cursor or null on the last page.
has_morebooleanWhether another page exists.

GET /api/v1/feedback/:id

GET/api/v1/feedback/{id}

Full solving context for one report in the key’s project. Optional query include=recording_events adds the widget timeline. rrweb events are never inlined; use the signed URLs under replay.

http

GET /api/v1/feedback/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee?include=recording_events
Authorization: Bearer $FIXNOTY_API_KEY

Identity

FieldTypeDescription
iduuidReport id.
project_iduuidAlways the project bound to the key.
pin_numbernumberPin number.
status / priority / category / feedback_typeenumSame enumerations as the list endpoint.
created_at / updated_at / last_activity_atdatetimeUTC timestamps.
resolved_at / archived_atdatetime | nullSet when the report left the open set.

Text / brief

FieldTypeDescription
title / commentstringTitle may be null; comment is the primary client text.
user_goal / actual_result / expected_resultstring | nullStructured bug fields when the client filled them.
reproducibility / impactstring | nullOptional severity context.
affected_devicesstring[] | nullDevices the client mentioned.
client_title / client_descriptionstring | null“New request” brief.
waiting_for_customer_reasonstring | nullWhy the report is waiting on the client.

Location

FieldTypeDescription
page_url / page_pathstringWhere the pin was placed.
positionobject{ x, y, x_percent, y_percent } click/pin coordinates.
selectionobject{ type: point | area | none, rect_*_percent } selected rectangle.
scrollobject{ x, y } scroll offsets.
viewportobject{ width, height } layout viewport at capture.

Environment

FieldTypeDescription
environment.user_agentstring | nullRaw UA string.
environment.browser / devicestring | nullParsed browser and device class.
environment.recording_metadataobject | nullviewport_width/height, visual_viewport_*, device_pixel_ratio, orientation, screen_*, touch_support, pointer_events, device_class, platform, pages_visited, event_count, replay_segment_count, session_id. Secrets stripped.

People

FieldTypeDescription
authorobject{ name, email }. Email may be null.
assigned_toobject | null{ user_id, name, email }.
visible_to_clientbooleanWhether the client portal shows this report.
client_confirmed_at / client_reopened_atdatetime | nullPortal confirmation / reopen.

Media

FieldTypeDescription
screenshotsigned | null30-minute signed JPEG/PNG/WebP. screenshot_captured_at is when it was taken.
attachments[]arrayWidget files with signed URLs (30 min).
debug_attachments[]arrayFiles captured during Record issue (30 min).
replayobject | nullSee replay package. playback is always “rrweb”. video is an alias of formats.video.file when a legacy MP4 exists.

Debug, conversation, flags

FieldTypeDescription
console_logsarray{ type: error | unhandledrejection, message, stack?, url?, timestamp_ms }.
recording_eventsarray | nullWidget timeline. null unless include=recording_events.
recording_warningsarray{ code, message } from capture.
debug_summary / captured_valuesobject | array | nullSanitized advanced-debug payload. Sensitive keys are stripped recursively.
conversation[]arrayTeam notes + client comments, chronological. { id, body, created_at, visibility: internal | client, source: team | client, author_name }.
activity[]array{ id, action, old_value, new_value, created_at }. action is one of the activity enumerations below. created, status_changed, priority_changed, category_changed, internal_notes_updated, customer_update_added, waiting_reason_updated, feedback_resolved, feedback_archived, client_comment_added, client_confirmed_resolution, client_reopened_task, visibility_changed, screenshot_captured, replay_attached, file_attached, client_context_added, title_updated, assignee_changed.
flagsobjecthas_screenshot, has_replay, has_attachments, has_console_errors, has_advanced_data, has_debug_files.
Never returned: pin_hash, cookies, service-role material, key_hash, access_token_hash, unsigned storage paths, Authorization values. Nested objects are recursively stripped of those keys before serialization.

GET /api/v1/feedback/:id/assets

GET/api/v1/feedback/{id}/assets

Re-mints signed URLs without re-downloading comments, conversation, or activity. Top-level expires_in is the screenshot/attachment TTL (1800s). replay_expires_in is the player/rrweb TTL (86400s).

json

{
  "data": {
    "screenshot": {
      "url": "https://….supabase.co/storage/v1/object/sign/feedback-screenshots/…?token=redacted",
      "expires_in": 1800,
      "expires_at": "2026-10-02T10:30:00.000Z"
    },
    "attachments": [],
    "debug_attachments": [],
    "replay": null
  },
  "expires_in": 1800,
  "replay_expires_in": 86400
}

GET /api/v1/feedback/:id/replay

GET/api/v1/feedback/{id}/replay

Dedicated replay envelope. Widget timeline is included by default. Open formats.player.watch.url in a browser to see the session exactly as in the dashboard.

json

{
  "data": {
    "feedback_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
    "has_replay": true,
    "duration_seconds": 42,
    "recording_session_id": null,
    "debug_session_id": "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff",
    "expires_in": 86400,
    "formats": {
      "rrweb": {
        "available": true,
        "playback": "rrweb",
        "expires_in": 86400,
        "manifest": {
          "url": "https://….supabase.co/storage/v1/object/sign/debug-replays/…?token=redacted",
          "expires_in": 86400,
          "expires_at": "2026-10-03T10:00:00.000Z"
        },
        "chunks": [
          {
            "url": "https://…/chunk-000.json?token=redacted",
            "expires_in": 86400,
            "expires_at": "2026-10-03T10:00:00.000Z"
          }
        ]
      },
      "player": {
        "available": true,
        "playback": "player",
        "expires_in": 86400,
        "watch": {
          "url": "https://www.fixnoty.com/it/replay/<signed-token>",
          "expires_in": 86400,
          "expires_at": "2026-10-03T10:00:00.000Z"
        }
      },
      "video": {
        "available": false,
        "playback": "mp4",
        "status": "not_generated",
        "mime_type": "video/mp4",
        "expires_in": 86400,
        "file": null
      },
      "still": {
        "url": "https://….supabase.co/storage/v1/object/sign/feedback-screenshots/…?token=redacted",
        "expires_in": 1800,
        "expires_at": "2026-10-02T10:30:00.000Z"
      }
    },
    "timeline": [
      {
        "t": 1200,
        "at": "2026-10-01T14:12:01.200Z",
        "type": "click",
        "page_url": "https://staging.example.com/checkout"
      }
    ],
    "warnings": []
  },
  "expires_in": 86400
}

Using rrweb chunks

  1. GET the signed manifest URL.
  2. GET each chunk URL in array order.
  3. Concatenate the event arrays.
  4. Pass the result to an rrweb player. This is not an MP4.

Enumerations

status

aperto | in_lavorazione | in_attesa_cliente | risolto | archiviato

priority

normale | importante | bloccante

category

testo | design | immagine | bug | mobile | contenuto | altro

feedback_type

visual | debug_replay | text_content | request

recording event type

widget_loaded · widget_reconnected · click · tap · pointer_down · pointer_up · scroll · navigation · input_focus · input_change_masked · password_field_interaction_masked · keydown_masked · file_selection_masked · console_error · js_error · unhandled_rejection · visibility_change · pagehide · beforeunload · debug_started · debug_stopped · replay_started · replay_stopped · replay_snapshot · input_value_captured · input_value_masked · dropdown_open · dropdown_selection · picker_open · picker_selection · file_selected · file_attached · file_attachment_skipped · orientation_change · viewport_resize · visual_viewport_resize · tab_open

Timeline items: { t, at, type, page_url?, from?, to?, x?, y?, … }. Password and sensitive inputs are masked at capture (*_masked types).

Consuming with an AI system

This API is a data channel. It does not call a model and does not auto-fix reports.

  • Text-only LLM. Read list + detail JSON, console logs, timeline, and conversation. You get actions, errors, copy, and one still frame. You do not get motion.
  • Browser-capable agent. Open formats.player.watch.url, press play, and screenshot or watch the reconstructed DOM. A bare HTTP GET of that URL returns the player HTML/JS shell, not video frames.
  • Custom player. Download formats.rrweb chunks. Same events the dashboard uses.

We do not generate MP4 from rrweb: it would duplicate storage already held as event chunks. The 24-hour player reuses those chunks.

Security model

  • This documentation URL is public on purpose. Public docs describe the contract. They do not grant access.
  • Access control is the project API key (hashed at rest) plus server-side project_id filters. Guessing another project’s report UUID cannot leak it.
  • Dashboard RLS: only the owner can select/insert/update rows in project_api_keys.
  • Player links are unlisted, unindexed, non-embeddable, time-bounded, and key-revocable. Do not forward them.
  • Do not put API keys in frontend bundles or in Referer-leaking pages. Prefer server-side callers.

curl examples

bash

export FIXNOTY_API_KEY='fn_live_…'
export APP_URL='https://www.fixnoty.com'

# Sanity-check the key
curl -sS "$APP_URL/api/v1/project" \
  -H "Authorization: Bearer $FIXNOTY_API_KEY"

# Open reports (default)
curl -sS "$APP_URL/api/v1/feedback?status=open&limit=50" \
  -H "Authorization: Bearer $FIXNOTY_API_KEY"

# Every status since 1 Oct 2026
curl -sS "$APP_URL/api/v1/feedback?status=all&updated_since=2026-10-01T00:00:00.000Z" \
  -H "Authorization: Bearer $FIXNOTY_API_KEY"

# Full detail + widget timeline
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID?include=recording_events" \
  -H "Authorization: Bearer $FIXNOTY_API_KEY"

# Refresh signed media only
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID/assets" \
  -H "X-Api-Key: $FIXNOTY_API_KEY"

# Replay package: 24h player + rrweb + still + timeline
curl -sS "$APP_URL/api/v1/feedback/FEEDBACK_ID/replay" \
  -H "Authorization: Bearer $FIXNOTY_API_KEY"

Versioning and out of scope

Current version: v1. Additive fields may appear. The following are explicitly out of v1:

  • POST / PATCH / DELETE (status, comments, assignment)
  • Synchronous zip of a whole project
  • Billing, paid rate plans, webhooks
  • Official SDKs and OpenAPI
  • Model inference, auto-fix, or outbound AI webhooks
  • MP4 transcode from rrweb

Canonical public reference: this page. A matching technical note lives in the repository at docs/api-v1.md. Browser “Print → Save as PDF” is supported; we do not ship a separately versioned PDF that can drift from the implementation.

Measurement cookies

Sign-in cookies stay on. Analytics and ads start only if you allow them. We never send email, names, or feedback text to Google. Cookie notice