Ingest API
Send error and event payloads to Pulse. Use your project API key from project settings.
Endpoint
POST https://pulse.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> - 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://pulse.dply.io/api/v1/ingest). 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 (Billing) 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).
Outbound webhooks (integrations)
In Project → Settings, add an outbound webhook URL. When the first event for a new fingerprint (issue) is stored, Pulse queues an HTTP POST with Content-Type: application/json. Header X-Pulse-Event: issue.created and X-Pulse-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 Pulse 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
All fields are optional except: at least one of message or exception_class is required.
| Field | Type | Description |
|---|---|---|
| message | string | Error or log message. Required if exception_class is omitted. |
| 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 | Stack trace text (e.g. from error.stack or exception backtrace). |
| context | object | Arbitrary key-value context (e.g. user id, request id). |
| url | string | Page or request URL where the error occurred. |
| environment | string | Deployment environment (e.g. production, staging). Shown as a tag and available for filtering—similar to Sentry. |
| 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). |
| 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, data (object). Shown as a timeline on the event page. |
| timestamp | string | ISO 8601 timestamp. Defaults to server time if omitted. |
| fingerprint | string | Optional. When provided (non-empty), used for grouping; otherwise the server computes a hash from message + exception_class + file + line. |
| 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. |
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>" } - 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 (Billing) 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 — Either the monthly event quota is reached (upgrade hint) or the organization has a failed Stripe invoice until payment succeeds (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://pulse.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/...",
"context": { "request_id": "req-abc" },
"environment": "production",
"release": "my-app@2.4.1",
"server_name": "web-1",
"user": { "id": "42", "email": "user@example.com" },
"breadcrumbs": [
{ "type": "navigation", "message": "GET /checkout", "timestamp": "2025-03-17T11:59:50Z" }
],
"url": "https://example.com/page",
"timestamp": "2025-03-17T12:00:00Z"
}'
Same request using Bearer instead of X-Api-Key:
curl -X POST "https://pulse.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('pulse.api_key'),
])
->post(config('pulse.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://pulse.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).