Ingest API
Send error and event payloads to Lookout. Use your project API key from project settings.
For errors, traces, and source maps at a glance, see Protocol overview. For Laravel and Rails breadcrumbs and exception hooks, see Framework integrations.
Endpoint
POST https://lookout.dply.io/api/ingest
Content-Type: application/json
Authentication
Pass your project API key using the first match below (highest priority first):
- Header:
X-Api-Key: <your_api_key> - Header (alias):
X-Api-Token: <your_api_key>— same value as the project API key; supported for clients that send this header name - Bearer:
Authorization: Bearer <your_api_key> - Query (GET only):
?api_key=...onGET /api/projects/{id}/events - Body:
api_keyin the JSON body (typical forPOST /api/ingest)
The key is validated against a single project. Invalid or missing key returns 401.
Versioned routes & read API
The same ingest and read handlers are exposed under /api/v1/... for forward compatibility (for example POST https://lookout.dply.io/api/v1/ingest or POST https://lookout.dply.io/api/v1/errors). Unversioned /api/ingest remains supported.
Team visibility vs API keys: Project API keys gate POST /api/ingest and GET /api/projects/{id}/events. Team-based project visibility applies to the signed-in web app only, not to these key-authenticated endpoints. Organization ingest IP allowlists (Organization settings) apply to every request authenticated with a project API key (ingest and read-events, including /api/v1/...), not to the browser UI.
Idempotency
Optional header Idempotency-Key (per project). Retries with the same key within 24 hours receive the same 202 response body and do not enqueue another event. Keys are stored in the application database (row expires after 24 hours).
Reliability signals (ingest-time)
When an event is stored, Lookout runs lightweight heuristics on the message, stack_trace, stack_frames, optional exception_chain, and exception_class—similar in spirit to “reliability” checks in tools like SymfonyInsight, but not a full repository scan. Results are saved as reliability_signals (JSON array of id, severity, optional category e.g. security, title, detail) and shown on the occurrence page in the app. Use CI static analysis for codebase-wide rules; this field highlights patterns visible in the failing trace.
Outbound webhooks (integrations)
In Project → Settings, add an outbound webhook URL. When the first event for a new fingerprint (issue) is stored, Lookout queues an HTTP POST with Content-Type: application/json. Header X-Lookout-Event: issue.created and X-Lookout-Signature: sha256=<hmac> where <hmac> is hex-encoded HMAC-SHA256 of the raw request body (the exact UTF-8 bytes sent as the entity body) using your signing secret (shown once when the webhook is created). Payload includes event, version, project, issue (fingerprint, first event id, message, level, etc.), and urls.event to open the event in the Lookout UI.
Retries & verification
Verify signatures using the same raw body your endpoint receives (before JSON parsing). HTTP 5xx and 429 responses are retried up to 4 attempts with increasing backoff (10s, 60s, 300s) when delivery runs on a queue worker (not the sync driver). Other status codes (including 4xx) are logged and not retried. Recent attempts appear under Project → Settings → Outbound webhooks.
Request body field reference
All fields are optional except: at least one of message or exception_class is required.
message(string) — Error or log message. Required if exception_class is omitted. Up to ~128 KB.exception_class(string) — Exception/error class name. Required if message is omitted.level(string) — One of: error, warning, info, debug.file(string) — Source file path or URL.line(integer) — Line number.stack_trace(string or array) — Stack trace text (e.g. from error.stack or exception backtrace). Arrays are JSON-encoded. Max ~524 KB when accepted.stack_frames(array) — Optional. Structured frames (e.g. from PHP Throwable::getTrace()): up to 200 objects with file, line, class, function, type (-> / ::), and optional call (full display string). The event UI prefers this for syntax-style coloring and line numbers.context(object) — Arbitrary key-value context (e.g. user id, request id).url(string) — Page or request URL where the error occurred.route(string) — Optional. Framework route name or pattern (e.g. api.users.show). Stored as issue_route; rolled up on the issue insights panel with URLs and transactions.component(string) — Optional. UI component name for front-end errors. Stored as issue_component; shown in issue insights.environment(string) — Deployment environment (e.g. production, staging). Shown as a tag and available for filtering.release(string) — Release or app version (e.g. git SHA, semver). Filterable in the UI and API. Each distinct value is also listed on the project Releases page (first/last seen and open issue counts).commit_sha(string) — Optional. Git commit the running build was produced from (up to 64 hex chars). Shown on the event page; when the project has github_repo set, links to GitHub.deployed_at(number or string) — Optional. When this build or image was produced (Unix seconds or ISO 8601). Pairs with POST /api/ingest/deploy for “what changed?” in the UI.server_name(string) — Host or server identifier where the event was captured.user(object) — Optional. Affected user: id, email, username, name, ip_address. Merged into context.user for display on the event page.breadcrumbs(array) — Up to 50 items; each may include type, category, message, level, timestamp, optional span_id (or data.span_id) to line up with trace spans in the UI, and data (object). Shown as a timeline on the event page. The Laravel SDK may send type/category glow for manual highlights (GlowBreadcrumb::glow(); see Framework integrations). Recommended defaults for clients: record navigation / route or URL changes, key user actions (submit, tab, modal), and API or fetch failures as breadcrumbs so triage shows what happened before the error.timestamp(string) — ISO 8601 timestamp. Defaults to server time if omitted.fingerprint(string) — Optional. When provided (non-empty), used for grouping. Otherwise the server uses the project’s Error grouping algorithm (see Project → Settings → Error grouping): default (message + class + file + line), PHP — first application stack frame (stage + class + first non-vendor frame, with optional application path), or JavaScript — normalized message (stage + class + normalized message). Admins can add optional Custom fingerprint rules in the same settings section (strip UUIDs, normalize URLs in messages, ignore stack frame prefixes) applied only before the hash is computed.grouping_override(string) — Optional. Up to 64 characters. When fingerprint is empty, treated as the custom grouping key (same storage as fingerprint). CamelCase alias from some PHP clients: overriddenGrouping.grouping_slow_path(boolean) — Optional. When true and you did not send a custom fingerprint / grouping_override, the server applies an extra hash over the normal fingerprint using route/transaction/url path + DB time bucket so slow or DB-heavy occurrences can split into their own issue. Pair with grouping_db_time_ms or grouping_db_time_ms_bucket. The PHP SDK can set this automatically when LOOKOUT_REPORT_PERFORMANCE_GROUPING=true and performance spans were recorded in the same request.grouping_db_time_ms(integer) — Optional. Total DB time in milliseconds for bucketing when grouping_slow_path is true. If omitted but grouping_db_time_ms_bucket is set, the explicit bucket string is used.grouping_db_time_ms_bucket(string) — Optional. Up to 32 chars. Your own bucket label (e.g. heavy) instead of deriving from grouping_db_time_ms.is_log(boolean) — Optional. Marks the occurrence as log-sourced vs a thrown exception. Default false. CamelCase alias: isLog.open_frame_index(integer) — Optional. Zero-based index into stack_frames (or client stack) the UI may highlight as the “open” frame. CamelCase alias: openFrameIndex.solution(string) — Optional. Pre-computed AI or Ignition-style suggestion (Markdown). Stored on the event; Lookout will not call OpenAI when this or ai_solution is present.ai_solution(string or object) — Optional. Same as solution when a string. As an object, may include solution / text, optional confidence (1–10), and optional should_display / display. Max ~32 KB when JSON-encoded.application_path(string) — Optional. Alias: grouping_application_path. For the PHP — first application stack frame algorithm, strip this absolute prefix from frame paths for this event only (overrides the project default).source_map_url(string) — Optional. URL to a source map (for future symbolication). Stored but not processed yet. Must be a valid URL if provided.correlation_id(string) — Optional. Shared id for one logical operation (request id, workflow id, etc.). The event page lists other events with the same value; GET /api/projects/{id}/events?correlation_id=... filters the read API. Prefer trace_id below for distributed tracing.request_id(string) — Optional. Top-level HTTP or app request id (up to 128 chars). Shown as a tag, used for log search URL templates in project settings, and recommended over only nesting under context so operators can copy it in one place. You may still duplicate in context.request_id for backward compatibility.occurrence_uuid(UUID string) — Optional. Stable id for this occurrence. Alias: tracking_uuid. If omitted, the server assigns one. Returned on the read API as occurrence_uuid and tracking_uuid.tracking_uuid(UUID string) — Optional. Same as occurrence_uuid when you prefer this field name.entry_type(string) — Optional. One of web, job, command, n_plus_one (performance insight from trace ingest), performance. If omitted, inferred from url, job/job_class, or command/command_line.job(string) — Optional. Queue job class name (alias for job_class). Powers issue insights and search job:….job_class(string) — Optional. Same as job.command(string) — Optional. CLI command (alias for command_line). Search: command:….command_line(string) — Optional. Same as command.handled(boolean) — Optional. When true, marks the occurrence as handled. Default false. Search: is:handled / is:unhandled in the project UI.language(string) — Optional. php or javascript (alias js). If omitted, inferred from exception_class, file extension, and common JS error names. Search: language:php / language:javascript.trace_id(string) — Optional. Distributed trace identifier (often 32 hex chars, same role as OpenTelemetry trace id). Stored on the error; pairs with spans sent to POST /api/ingest/trace. Filter: ?trace_id=... on the read API.span_id(string) — Optional. Span id for the operation where the error occurred (often 16 hex chars).parent_span_id(string) — Optional. Parent span id when nesting operations.transaction(string) — Optional. Root transaction or operation name (e.g. GET /api/orders). Stored as transaction_name on the event.trace(object) — Optional. Alternative to flat fields: trace.trace_id, trace.span_id, trace.parent_span_id, trace.transaction.lookout_trace(string) — Optional. Compact trace propagation value: {32-hex-trace}-{16-hex-span}-{0|1|?} (optional sampled flag; ? = deferred). Parsed into trace_id and span_id when those are not already set. Equivalent aliases: compact_trace, sentry_trace (same format as the sentry-trace HTTP header value).parent_id(ULID string) — Optional. Lookout error_events.id of a prior event in this project (e.g. root failure before a wrapped error). Shown as parent/child links in the UI. Must exist in the project or validation fails.exception_chain(array) — Optional. Nested causes (PHP getPrevious(), JS error.cause, etc.): up to 32 objects with optional exception_class, message, file, line, stack_trace. Rendered as a chain on the event page alongside the primary stack trace.
Searching issues in the app
On Project → Events, the search box matches structured directives plus free text. Directives include class:, url:, version: / release:, is:regression (grouped view: issues with more than one distinct release), handled:>30% (grouped issues), users_affected:>10 (grouped), language:php, occurrence_count_historic: (same as occurrence count here), and status/env/type/job/command/priority/user fields. Free text searches message, class, URL, job, and command; use * wildcards, -term to exclude (after a positive term), and "quoted phrase". See the placeholder on the project Events page for a concise cheat sheet.
Deployment markers & fixed-in-release workflow
Record deploys separately from errors so the organization dashboard can show markers next to the 7-day error chart. Same API key as error ingest.
Deploy ingest
POST https://lookout.dply.io/api/ingest/deploy — JSON body:
- release (required) — string, same convention as error
release(e.g. git SHA or version). - environment (optional) — string.
- deployed_at (optional) — ISO 8601 string or Unix timestamp; defaults to now.
- commit_sha (optional) — string.
- source (optional) — one of
api,ci,webhook,manual. - metadata (optional) — object (stored with the deployment row).
Response 201 with { "ok": true, "release": "…", "deployed_at": "…" }. Does not consume error quota. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/deploy.
Mark fixed in release (Rollbar-style)
On Project → Events (grouped view), resolve an issue with Fixed in release and a release string. New occurrences that match the same fingerprint and send the same non-empty release are stored as resolved automatically. If a new occurrence arrives with a different release (or no release), the group is reopened so you can treat it as a regression.
Distributed tracing & performance spans
Lookout uses a Lookout-native JSON model: link errors to a trace with the fields above, then send span trees to the dedicated endpoint below. Map your instrumentation (OpenTelemetry-style ids, transaction names, spans) to these payloads.
Span ingest endpoint
POST https://lookout.dply.io/api/ingest/trace — same authentication and rate limit as error ingest. Body (JSON):
- trace_id (required) — string, up to 64 characters (
[a-zA-Z0-9_.:-]). - spans (required) — array of span objects (max 200 per request, configurable via
LOOKOUT_TRACE_SPANS_MAX_PER_REQUEST). - transaction, environment, release, commit_sha, deployed_at — optional metadata (same meaning as error ingest); the root span without a description may inherit
transactionas its description.
Each span object includes: span_id (hex string), optional parent_span_id, op (e.g. http.server, db.query), optional description, required start_timestamp (Unix seconds or milliseconds), and either end_timestamp or duration_ms. Optional status and data (object) are stored as-is.
Response 202: { "accepted": true, "trace_id": "...", "spans": N }. The UI shows a timeline on the event page when the error’s trace_id matches stored spans. On Project → Traces → [trace id], append ?highlight_span=<span_id> to scroll the waterfall to a row (links from the event page include this when span_id was ingested).
Versioned alias: POST https://lookout.dply.io/api/v1/ingest/trace.
Project gate: trace ingest can be turned off per project in Project settings → Monitoring modes. When disabled, this endpoint returns 403; error ingest on POST /api/ingest is unchanged. See Monitoring modes.
OpenTelemetry OTLP JSON import
POST https://lookout.dply.io/api/ingest/trace/otlp — same authentication, rate limit, and performance ingest gate as native trace ingest. Body is the OTLP trace JSON export shape: top-level resourceSpans (array), each with optional resource.attributes and scopeSpans[].spans[] using Protobuf-JSON field names (traceId, spanId, startTimeUnixNano, endTimeUnixNano, kind, attributes, status, …).
- Multiple traces — distinct
traceIdvalues in one POST each become a separate stored trace (one async job per trace). - Resource → project fields —
deployment.environmentandservice.versionmap to Lookoutenvironment/releaseunless you override with top-level JSON keys of the same names. - Span attributes — stored on each span’s
data(string / number / bool values; capped per span). - Limits — total spans after conversion must not exceed
200(same as native ingest).
Response 202: { "accepted": true, "format": "otlp_json", "traces": [ { "trace_id": "…", "spans": N }, … ] }. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/trace/otlp.
PHP (no OTLP SDK required): Lookout\Tracing\Interop\OpenTelemetryTraceConverter::toJobPayloads($decodedJson) for the same mapping; fromLookoutIngestBody() builds OTLP JSON from a native Lookout trace body for exporters or tests.
PHP: lookout/tracing package
Install the first-party Composer library for Sentry-compatible propagation (sentry-trace, baggage), manual transactions/spans, optional Guzzle middleware, and Laravel middleware — aligned with Sentry PHP tracing instrumentation and trace propagation patterns (no Sentry SDK required).
composer require lookout/tracing
use Lookout\Tracing\Tracer;
use Lookout\Tracing\SpanOperation;
use Lookout\Tracing\Tracing;
// Incoming (e.g. middleware): same idea as Sentry\continueTrace()
Tracer::instance()->continueTrace(
$request->header('sentry-trace'),
$request->header('baggage'),
);
$tx = Tracing::startTransaction('GET /checkout', SpanOperation::HTTP_SERVER);
Tracing::trace(function () {
// work…
}, SpanOperation::DB_QUERY, 'SELECT …');
$tx->finish();
// Outgoing HTTP headers for downstream services
$headers = Tracer::instance()->outgoingTraceHeaders();
// [ 'sentry-trace' => '…', 'baggage' => '…' ]
// Error payload fields + POST /api/ingest/trace body via Tracer::flush() when configured
In this repo the package lives at packages/lookout-tracing and is required via a Composer path repository. Laravel: register middleware lookoutTracing.continueTrace (alias from the service provider) on your web/API stack; publish config with php artisan vendor:publish --tag=lookout-tracing-config for LOOKOUT_API_KEY, LOOKOUT_BASE_URI, and optional LOOKOUT_TRACING_AUTO_FLUSH. Set LOOKOUT_RELEASE, LOOKOUT_COMMIT_SHA, and LOOKOUT_DEPLOYED_AT (or rely on SOURCE_VERSION / GITHUB_SHA / similar) so traces and errors carry the same markers as deploy ingest.
Sampling and overhead (traces)
Self-hosted Lookout does not apply a sample rate inside POST /api/ingest/trace: spans you send are stored (subject to per-project limits, HTTP throttles, payload caps, and the performance ingest switch). Treat volume like other observability products: in production, send fewer traces — the same idea as lowering traces_sample_rate when you only need representative coverage.
- PHP (
lookout/tracing): head sampling viaLOOKOUT_PERFORMANCE_SAMPLE_RATE(default0.1) andperformance.samplerin published config. Traces continued fromsentry-tracewithsampled=0never record spans. Optional tail sampling records spans locally, then drops boring batches at flush unless the root transaction is slow, the response is an error, a span reports an error, you keep a residual random fraction, or the trace participates downstream — seeLOOKOUT_PERFORMANCE_TAIL_SAMPLING,LOOKOUT_PERFORMANCE_TAIL_SLOW_MS,LOOKOUT_PERFORMANCE_TAIL_RESIDUAL_RATE, andperformance.tail_samplinginlookout-tracing.php. - Any language or generic PHP app: decide before POST whether to export (fixed percentage, rules per route, or “slow only” using wall time or root span duration). Align with your SLA: e.g. always capture when duration exceeds N ms, and otherwise sample lightly.
Real User Monitoring (RUM)
Optional browser beacons for Core Web Vitals (LCP, INP, CLS, FCP), time to first byte from Navigation Timing, and SPA / Livewire navigations. Same API key as error ingest. Uses the performance ingest project switch (with trace spans): when that is off, this endpoint returns 403.
RUM ingest endpoint
POST https://lookout.dply.io/api/ingest/rum — JSON body. Put api_key in the JSON when using navigator.sendBeacon (no custom headers). Otherwise use X-Api-Key like other ingest routes.
Rate limit: 120 requests per minute per project (LOOKOUT_RUM_INGEST_RATE_LIMIT). Does not count toward the monthly error quota.
- page_url (required) — full URL string (max 2048 chars).
- navigation_type (required) — one of:
load,visibility_change,livewire_navigate,popstate,manual,spa_transition. - client_route (optional) — client-side route label (path, named route, etc.).
- trace_id (optional) — 32 hex chars, same as server trace /
sentry-tracetrace id for correlation. - lcp_ms, inp_ms, fcp_ms, ttfb_ms (optional) — integers ≥ 0.
- cls (optional) — cumulative layout shift score (number).
- environment, release, user_agent (optional).
- extra (optional) — small object (≤ 32 keys) for forward compatibility.
Response 202: { "accepted": true, "id": "…" }. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/rum. View rows under Project → RUM.
Browser snippet
Shipped with the lookout/tracing package as resources/rum/lookout-rum.js (vanilla IIFE, no npm dependency). Call LookoutRum.init({ endpoint, apiKey, … }). For Laravel + Livewire, enable livewireNavigate: true to send livewire_navigate beacons on livewire:navigated. Expose the server trace_id in a <meta name="lookout-trace-id" content="…"> (or implement traceId in init) to link RUM rows to traces.
CPU profiling
Upload flame graphs and profiler dumps the same way as Sentry PHP profiling expects you to produce data locally (Excimer → speedscope JSON, xhprof/Tideways trees, SPX, etc.); Lookout stores the payload and lists it under Project → Profiles. Open speedscope format in speedscope.
Profile ingest endpoint
POST https://lookout.dply.io/api/ingest/profile — same X-Api-Key as error ingest. Does not count toward the monthly error quota; blocked when billing is delinquent (same as trace / cron ingest).
Rate limit: 30 requests per minute per project (configurable via LOOKOUT_PROFILE_INGEST_RATE_LIMIT).
Payload size: the JSON-encoded data object may not exceed 2048 KB (configurable via LOOKOUT_PROFILE_INGEST_MAX_KB, capped server-side).
JSON body:
- agent (required) — one of:
excimer,xhprof,tideways,spx,php.manual_pulse(cooperative sampling, no extension),lookout(first-party aggregate hotspots),blackfire,datadog,xdebug,newrelic,otel.php,other. - format (required) — e.g.
speedscope,xhprof,spx,lookout.v1(aggregate),lookout.samples.v1(time-series stacks fromManualPulseSampler). - data (required) — arbitrary JSON for vendor formats; for
lookout.v1/lookout.samples.v1the server normalizes and setsschema_version: 1(see below). - trace_id, transaction, environment, release, commit_sha, deployed_at — optional metadata (same roles as trace / error ingest).
Response 202: { "accepted": true, "id": N }. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/profile.
First-party formats: lookout.v1 & lookout.samples.v1
Use lookout.v1 with agent: lookout when you already have per-location sample counts (from your own collector or an adapter). The UI shows a Hotspots table for these formats.
{
"schema_version": 1,
"frames": [
{ "file": "app/Services/Checkout.php", "line": 120, "samples": 48 },
{ "file": "vendor/…/Connection.php", "line": 410, "samples": 12 }
],
"meta": { "note": "optional small map" }
}
lookout.samples.v1 carries time-ordered stack captures: started_at, ended_at, samples as an array of { "t": <unix float>, "frames": [ { "file", "line", "function", … } ] }. The server fills schema_version and sample_count on ingest; hotspots are derived by counting frame appearances (capped for very large payloads).
PHP: lookout/tracing package
Use Lookout\Tracing\Profiling\ProfileClient::sendProfile() after ProfileClient::configure([...]) (Laravel loads this from published lookout-tracing config alongside the tracer and cron client). Helpers build payloads for Excimer (speedscope), xhprof/Tideways, SPX, cooperative php.manual_pulse sampling (lookout.samples.v1), and LookoutProfileV1Payload::aggregateIngestBody() for lookout.v1.
Sampling and overhead (profiles)
Profile ingest has no server-side profiles_sample_rate: you choose when to POST. CPU profilers (Excimer, Tideways, Xdebug, Blackfire, etc.) add measurable overhead; self-hosted Lookout accepts what you send subject to the per-project request rate limit and JSON size cap above, so cap volume in your application.
- Random sampling: upload for a small percentage of requests or jobs.
- SLA-style triggers: profile only when wall time or your root span duration exceeds a threshold; skip fast paths entirely.
- Correlate with traces: enable profiling only when the same request is already traced (or marked slow) so stored profiles match the transactions you kept.
Logs
Send structured application logs (same spirit as Sentry structured logs for PHP: level, message, attributes) or plain text lines from nginx, Apache, syslog, or any shipper that can POST JSON. View entries under Project → Logs.
Log ingest endpoint
POST https://lookout.dply.io/api/ingest/log — same X-Api-Key (or bearer / body api_key) as error ingest. Does not count toward the monthly error quota; returns 402 when billing is delinquent; returns 403 when Accept log ingest is off in project settings.
Rate limit: 300 requests per minute per project (LOOKOUT_LOG_INGEST_RATE_LIMIT). Batch size: up to 200 entries per request (LOOKOUT_LOG_INGEST_MAX_ENTRIES); JSON-encoded entries may not exceed 1024 KB (LOOKOUT_LOG_INGEST_MAX_REQUEST_KB).
Payload shapes
Single entry (object at the root):
- message (required) — UTF-8 string (max 256 KB per entry).
- level (optional) — e.g.
trace,debug,info,warn/warning,error,fatal, or a custom short string. - timestamp (optional) — Unix seconds, Unix milliseconds, or ISO 8601; defaults to ingest time.
- attributes (optional) — object (≤ 64 keys) for searchable metadata; values should be scalars or small arrays (nested objects are JSON-stringified).
- logger (optional) — channel name (e.g. Monolog channel).
- source (optional) — e.g.
php,nginx.access,nginx.error,apache.error,syslog. - environment, release, trace_id, hostname — optional (same idea as other ingest endpoints).
Batch — entries: array of objects with the same fields as above (each row requires message).
Plain lines (nginx / Apache access logs, etc.) — send lines: string array; each string becomes one row. Optional top-level source defaults to nginx.access.
{
"lines": ["127.0.0.1 - - [25/Mar/2026:10:00:00 +0000] \"GET / HTTP/1.1\" 200 612"],
"source": "nginx.access"
}
Response 202: { "accepted": true, "count": N, "ids": [ "…" ] }. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/log.
CLI shipper (lookout/cli)
Install the same Lookout CLI you use for REST triage (composer global require lookout/cli or path repo). The ship-logs command reads lines from a file or stdin and POSTs them in batches with your project API key (not your personal access token):
export LOOKOUT_BASE_URL="https://your-lookout-host"
export LOOKOUT_PROJECT_API_KEY="…"
tail -F /var/log/nginx/access.log | lookout ship-logs --source=nginx.access
See the CLI README in the lookout/cli package for --batch-size, --dry-run, and a sample systemd unit. For PHP (Laravel), the lookout/tracing package exposes lookout_logger() plus an optional Monolog LookoutMonologHandler (LOOKOUT_LOGS_ENABLED); see that package README. For other stacks or agents, use your logger or a forwarder (Vector, Fluent Bit, Logstash) to POST entries arrays to this endpoint.
Custom metrics
Record counters, gauges, and distributions in the same spirit as Sentry’s PHP metrics. Lookout stores raw samples (with optional trace_id for correlation) and shows rollups plus recent points under Project → Metrics—not a separate metrics SKU, just another signal next to logs and traces.
Metric ingest endpoint
POST https://lookout.dply.io/api/ingest/metric — same X-Api-Key (or bearer / body api_key) as error ingest. Does not count toward the monthly error quota; returns 402 when billing is delinquent; returns 403 when Accept custom metric ingest is off in project settings.
Rate limit: 180 requests per minute per project (LOOKOUT_METRIC_INGEST_RATE_LIMIT). Batch size: up to 100 entries per request (LOOKOUT_METRIC_INGEST_MAX_ENTRIES); JSON-encoded entries may not exceed 512 KB (LOOKOUT_METRIC_INGEST_MAX_REQUEST_KB).
Payload
Batch — entries: array of objects, each with:
- name (required) — metric name: starts with a letter; letters, digits,
_,.,:,/,-(max 128). - kind (required) —
counter(increment byvalue),gauge, ordistribution. - value (required) — finite number (counter delta or observed sample).
- unit (optional) — short hint, e.g.
ms,s,By. - timestamp (optional) — same parsing as logs (Unix s/ms, ISO 8601); defaults to ingest time.
- attributes (optional) — object (≤ 64 keys), same sanitization rules as log attributes.
- environment, release, trace_id — optional context.
A single sample may be sent as one JSON object at the root with the same fields (omitting entries).
Response 202: { "accepted": true, "count": N, "ids": [ "…" ] }. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/metric.
PHP SDK: lookout/tracing → lookout_metrics(), LOOKOUT_METRICS_ENABLED. Samples use the same retention window as logs and errors (lookout:prune).
User feedback (crash reports)
Let people who hit an error page describe what they were doing—similar in spirit to Sentry user feedback, but stored as a thread comment on the specific occurrence in Lookout (not a separate inbox). Triagers see it next to internal notes on the event detail page.
Feedback ingest endpoint
POST https://lookout.dply.io/api/ingest/feedback — same X-Api-Key (or bearer / body api_key) as error ingest. Does not count toward the monthly error quota; returns 402 when billing is delinquent; returns 403 when Accept user feedback ingest is off in project settings.
Rate limit: 60 requests per minute per project (LOOKOUT_USER_FEEDBACK_INGEST_RATE_LIMIT).
JSON body
- comments (required) — free-text message from the user (max 10,000 chars). Alias:
message. - event_id — error occurrence ULID (same as
idon ingest response and read APIs). Alias:eventId. - occurrence_uuid — UUID stored on the error row (see occurrence_uuid on error ingest). Use this from browser or mobile when you only have the stable occurrence id.
Provide exactly one of event_id or occurrence_uuid so the comment attaches to one stored occurrence.
Optional reporter fields (shown on the thread, not used for auth):
- name — display name (max 128).
- email — contact email (validated format).
Response 202: { "accepted": true, "comment_id": "…", "error_event_id": "…" }. 404 if no occurrence matches this project. Versioned alias: POST https://lookout.dply.io/api/v1/ingest/feedback.
PHP SDK: lookout/tracing
When the SDK builds an error report it sets occurrence_uuid on the payload (or keeps yours) and exposes lookout_last_error_occurrence_uuid() / ErrorReportClient::lastOccurrenceUuid() for your custom error page—POST that value with comments to tie feedback to the same row Lookout already stored.
Cron monitors & check-ins
Lookout accepts Sentry Crons–style check-ins so you can record scheduled job runs (start / success / failure) per monitor slug. See Sentry’s PHP Crons docs for the conceptual model; you do not need the Sentry SDK.
Check-in endpoint
POST https://lookout.dply.io/api/ingest/cron — same X-Api-Key as other ingest. Does not count toward the monthly error quota; blocked when billing is delinquent (same as trace ingest).
Rate limit: 6 requests per minute per monitor slug and environment (configurable via LOOKOUT_CRON_INGEST_RATE_LIMIT), similar to common cron ingest limits.
JSON body:
- slug (required) — monitor identifier (
[a-zA-Z0-9][a-zA-Z0-9_.-]*, max 128). - status (required) —
in_progress,ok, orerror. - check_in_id (optional, UUID) — return value from a prior
in_progresscall; required to complete that run withok/error. Omit for one-shot heartbeat check-ins. - duration (optional) — seconds (e.g. heartbeat with measured runtime).
- environment (optional) — defaults to empty string; separate monitors per environment.
- monitor_config (optional) — upsert schedule and thresholds:
name,schedule({"type":"crontab","crontab":"*/10 * * * *"}or{"type":"interval","value":10,"unit":"minute"}),checkin_margin,max_runtime,timezone,failure_issue_threshold,recovery_threshold. - meta (optional) — up to 32 string keys (64 chars) with string/number/bool/small JSON values (values truncated for storage). Shown on the monitor detail page.
Response 202: { "accepted": true, "check_in_id": "<uuid>", "monitor_id": N }. Completing a run updates the same logical check-in row (status + duration). Versioned alias: POST https://lookout.dply.io/api/v1/ingest/cron.
When monitor_config.schedule is set, Lookout stores next expected by (schedule + check-in margin). A scheduled worker records missed check-ins if nothing arrives in time, and expires stale in_progress rows using max_runtime (default 24h if unset). Run php artisan schedule:work or a queue worker so DetectMissedCronMonitors runs every minute.
Project Crons lists monitors; open a row for check-in history, filters, and metadata.
PHP: lookout/tracing package (cron client)
use Lookout\Tracing\Cron\CheckInStatus;
use Lookout\Tracing\Cron\Client as CronClient;
use Lookout\Tracing\Cron\MonitorConfig;
use Lookout\Tracing\Cron\MonitorSchedule;
use Lookout\Tracing\Cron\ScheduleUnit;
CronClient::configure([
'api_key' => getenv('LOOKOUT_API_KEY'),
'base_uri' => 'https://your-lookout.example',
]);
$config = MonitorConfig::make(
MonitorSchedule::crontab('*/10 * * * *'),
checkinMarginMinutes: 5,
maxRuntimeMinutes: 15,
);
$id = CronClient::captureCheckIn('database-backup', CheckInStatus::inProgress(), monitorConfig: $config);
try {
runBackup();
CronClient::captureCheckIn('database-backup', CheckInStatus::ok(), checkInId: $id);
} catch (Throwable $e) {
CronClient::captureCheckIn('database-backup', CheckInStatus::error(), checkInId: $id);
throw $e;
}
// Or wrap a callback (auto ok / error + duration):
CronClient::withMonitor('nightly-job', fn () => doWork(), $config);
// Heartbeat (single ok, optional duration in seconds):
CronClient::captureCheckIn('heartbeat-job', CheckInStatus::ok(), null, 10.0);
Source maps (planned)
Future versions may support symbolication: turning minified JavaScript stack traces into readable file:line information using source maps. For now, the following optional fields are accepted and stored but not processed. Integrators can send them so that when symbolication is implemented, existing clients do not need to change.
- source_map — Base64-encoded source map JSON (inline map). Stored for later use; not yet used for symbolication.
- source_map_url — URL to fetch the source map (e.g.
https://example.com/static/app.js.map). Must be a valid URL if provided. Stored on the event. - minified_stack — Raw minified stack trace string for server-side symbolication. Not yet processed; reserved for future use.
No symbolication is performed today. These fields can be included in the request body and will be stored where supported (e.g. source_map_url is persisted on the error event).
Responses
- 202 Accepted — Event accepted. Body:
{ "id": "<uuid>" }When spike protection is active for the project (see Project → Settings), most ingests still return 202 but only one error per minute is persisted; the rest are dropped so your monthly quota is not exhausted by a flood. - 401 Unauthorized — Missing, invalid, or non-string API key. Body:
{ "message": "Missing or invalid API key." }or{ "message": "Invalid API key." } - 403 Forbidden — Organization admins configured an ingest IP allowlist (Organization settings) and the client IP does not match any allowed address or CIDR (applies to ingest and read-events API). Body:
{ "message": "API access is not allowed from this IP address." } - 402 Payment Required — Error ingest: monthly event quota reached (upgrade hint) or failed Stripe invoice. Trace ingest (
POST .../ingest/trace) is not counted toward the error quota; it is only rejected when billing is delinquent (same billing URL in body). - 422 Unprocessable Entity — Validation failed (e.g. neither
messagenorexception_classprovided, or invalidlevel). Body: JSON field errors (e.g.errorsobject). - 429 Too Many Requests — Rate limit exceeded (60 requests per minute per API key or per IP). Response includes a
Retry-Afterheader; retry after that time.
Example: cURL
curl -X POST "https://lookout.dply.io/api/ingest" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_PROJECT_API_KEY" \
-d '{
"message": "Something went wrong",
"exception_class": "RuntimeException",
"level": "error",
"file": "app/Http/Controllers/ExampleController.php",
"line": 42,
"stack_trace": "#0 vendor/...",
"request_id": "req-abc",
"context": { "cart_id": "c_9f2a" },
"environment": "production",
"release": "my-app@2.4.1",
"server_name": "web-1",
"user": { "id": "42", "email": "user@example.com" },
"breadcrumbs": [
{ "type": "navigation", "category": "route", "message": "GET /products", "level": "info", "timestamp": "2025-03-17T11:58:10Z" },
{ "type": "navigation", "category": "route", "message": "GET /checkout", "level": "info", "timestamp": "2025-03-17T11:59:50Z" },
{ "type": "user", "category": "ui.click", "message": "Submit order", "level": "info", "timestamp": "2025-03-17T11:59:58Z" }
],
"url": "https://example.com/page",
"timestamp": "2025-03-17T12:00:00Z"
}'
Same request using Bearer instead of X-Api-Key:
curl -X POST "https://lookout.dply.io/api/ingest" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_PROJECT_API_KEY" \
-d '{"message":"Something went wrong","level":"error"}'
Example: PHP
$response = \Illuminate\Support\Facades\Http::withHeaders([
'X-Api-Key' => config('lookout.api_key'),
])
->post(config('lookout.ingest_url') . '/api/ingest', [
'message' => $exception->getMessage(),
'exception_class' => get_class($exception),
'level' => 'error',
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'stack_trace' => $exception->getTraceAsString(),
'url' => request()->fullUrl(),
'timestamp' => now()->toIso8601String(),
]);
Example: JavaScript (fetch)
await fetch('https://lookout.dply.io/api/ingest', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'YOUR_PROJECT_API_KEY',
},
body: JSON.stringify({
message: error.message,
exception_class: error.name,
level: 'error',
file: error.fileName || window.location.href,
line: error.lineNumber,
stack_trace: error.stack,
url: window.location.href,
timestamp: new Date().toISOString(),
}),
});
Get your project API key from Project → Settings in the dashboard. Rate limit: 60 requests per minute per API key (or per IP if key is not resolved).