OwlMetry
API Reference

Attachments

Event attachment API — list, fetch, download, delete, and inspect project quota usage.

Manage event attachments — binary files (logs, screenshots, crash dumps) uploaded by SDKs alongside events. See Attachments concepts for quotas, storage, and cleanup behavior.

POST /v1/ingest/attachment

Reserve an event attachment. The SDK sends metadata (filename, size, SHA-256, content type, client_event_id, optional user_id). The server validates quotas and returns an upload_url to PUT the bytes to.

Auth required: Yes (client API key with events:write permission, scoped to an app)

Rate limited: Yes

Request body

{
  "client_event_id": "550e8400-e29b-41d4-a716-446655440000",
  "user_id": "user_42",
  "original_filename": "input.heic",
  "content_type": "image/heic",
  "size_bytes": 1048576,
  "sha256": "f1a2b3c4...",
  "is_dev": false
}
FieldTypeRequiredDescription
client_event_idstringYesLinks the attachment to its event (matches events.client_event_id).
original_filenamestringYesFilename shown in the dashboard. Max 255 chars.
content_typestringYesMIME type. Executables/scripts are rejected with 415.
size_bytesintegerYesDeclared size, used for quota checks before upload.
sha256stringYes64-char lowercase hex of the file's SHA-256. Verified during upload.
user_idstringNoEnd-user ID — enables the per-user quota check. Omit for backend apps that don't track end-users.
is_devbooleanNoWhether this is a development-build attachment. Default false.

Response

// Success (201)
{
  "attachment_id": "uuid",
  "upload_url": "https://api.owlmetry.com/v1/ingest/attachment/<id>",
  "expires_at": "2024-01-15T10:35:00.000Z"
}

Rejection codes (413)

CodeMeaning
user_quota_exhaustedPer-user bucket would be exceeded (requires user_id).
quota_exhaustedProject-wide quota would be exceeded.

Rejections include the effective quota and current usage. The calling event still posts — SDKs drop the attachment silently on rejection.

PUT /v1/ingest/attachment/:id

Stream the file bytes to the URL returned by POST /v1/ingest/attachment. The server verifies Content-Length against the reserved size_bytes and recomputes SHA-256 to match the reservation.

Auth required: Yes (same client API key used to reserve)

Headers: Content-Type: application/octet-stream

Response

// Success (200)
{
  "uploaded": true
}

Returns 409 already_uploaded if the reservation already has bytes, 410 if the reservation was soft-deleted, and 413 if the streamed body exceeds the declared size_bytes.

GET /v1/attachments

List attachments for the authenticated caller's teams. Ordered by created_at descending. Soft-deleted rows are excluded.

Auth required: Yes (events:read permission or JWT)

Query parameters

ParameterTypeDescription
event_idstringFilter to a specific event row (events.id).
event_client_idstringFilter by the client-provided event UUID. Useful when the event hasn't arrived yet — attachments are uploaded by the SDK in parallel with the event batch.
issue_idstringFilter to attachments linked to an issue.
project_idstringFilter to one project. Omit to get attachments across all teams the caller can access.
cursorstringISO timestamp cursor from the previous response.
limitnumberItems per page (1-200, default 50).

Response

{
  "attachments": [
    {
      "id": "uuid",
      "project_id": "uuid",
      "app_id": "uuid",
      "event_client_id": "uuid",
      "event_id": "uuid",
      "issue_id": "uuid",
      "user_id": "user_42",
      "original_filename": "crash.log",
      "content_type": "text/plain",
      "size_bytes": 4821,
      "sha256": "f1a2…",
      "is_dev": false,
      "uploaded_at": "2024-01-15T10:30:05.000Z",
      "created_at": "2024-01-15T10:30:00.000Z"
    }
  ],
  "cursor": "2024-01-15T10:30:00.000Z",
  "has_more": false
}

uploaded_at is null for attachments that have been reserved but not yet PUT. Callers should treat those as pending — no download URL is available until the upload completes.

GET /v1/attachments/:id

Fetch a single attachment with a signed download URL.

Auth required: Yes (events:read permission or JWT)

Response

{
  "id": "uuid",
  "project_id": "uuid",
  "app_id": "uuid",
  "event_client_id": "uuid",
  "event_id": "uuid",
  "issue_id": null,
  "user_id": "user_42",
  "original_filename": "crash.log",
  "content_type": "text/plain",
  "size_bytes": 4821,
  "sha256": "f1a2…",
  "is_dev": false,
  "uploaded_at": "2024-01-15T10:30:05.000Z",
  "created_at": "2024-01-15T10:30:00.000Z",
  "download_url": {
    "url": "https://api.owlmetry.com/v1/attachments/download?t=...",
    "expires_at": "2024-01-15T10:31:05.000Z",
    "original_filename": "crash.log",
    "content_type": "text/plain",
    "size_bytes": 4821
  }
}

The download_url object is omitted if the attachment has not yet been uploaded. Signed URLs expire 60 seconds after issuance (ATTACHMENT_DOWNLOAD_URL_TTL_SECONDS). Returns 404 if the attachment does not exist, is soft-deleted, or belongs to a team the caller cannot access.

GET /v1/attachments/download

Stream the attachment bytes. Requires a signed token — do not call this endpoint directly; use the download_url.url value from GET /v1/attachments/:id.

Auth required: No (token-based)

Query parameters

ParameterTypeDescription
tstringSigned HMAC token. Expires 60 seconds after being issued.

Returns the file bytes with Content-Type, Content-Length, Content-Disposition: attachment, X-Content-Type-Options: nosniff, and Cache-Control: private, no-store. If the server is configured with OWLMETRY_ATTACHMENTS_INTERNAL_URI, the response uses X-Accel-Redirect so nginx streams bytes directly without passing them through Node.

Returns 401 for missing/invalid/expired tokens. Returns 404 if the attachment no longer exists or has not completed upload.

DELETE /v1/attachments/:id

Soft-delete an attachment. The row is kept for 7 days (ATTACHMENT_SOFT_DELETE_GRACE_DAYS) before the attachment_cleanup job hard-deletes the bytes and database row.

Auth required: Yes (events:write permission; member role or higher)

// Response (200)
{
  "ok": true
}

Returns 404 if the attachment is missing or already soft-deleted. Returns 403 if the caller lacks the member role on the owning team.

GET /v1/projects/:projectId/attachment-usage

Return current attachment disk usage for a project, with optional per-user breakdown.

Auth required: Yes (events:read permission or JWT)

Query parameters

ParameterTypeDescription
user_idstringOptional. If supplied, includes user_used_bytes and user_file_count for that user.

Response

{
  "project_id": "uuid",
  "used_bytes": 104857600,
  "quota_bytes": 5368709120,
  "user_quota_bytes": 262144000,
  "file_count": 1842,
  "user_id": "user_42",
  "user_used_bytes": 12582912,
  "user_file_count": 37
}

quota_bytes and user_quota_bytes return the effective values — either the per-project override (attachment_project_quota_bytes / attachment_user_quota_bytes on projects) or the defaults (5 GB project, 250 MB per user). Returns 404 if the project is missing, soft-deleted, or not accessible to the caller.

  • Cleanup: the attachment_cleanup background job (daily 5am UTC) hard-deletes soft-deleted rows past the 7-day grace period and sweeps orphans. See Background jobs.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.