Attachments API

Upload receipts and PDFs and link them to bills, expenses, mileage, and bank transactions.

What this does

The Attachments API lets a bot or automation upload an image or PDF — typically a photo of a receipt — and then attach it to an existing record (bill, expense, mileage claim, or bank transaction).

The upload-then-link split is deliberate: a chat bot can accept the photo first ("got it, what's it for?"), confirm with the user which record to attach it to, and only call link once it's sure.

When to use it

  • A messaging bot wants to accept "snap of a receipt" inputs.
  • A scanner / inbox automation wants to push PDFs into Relentify.
  • A mobile app captures a photo before the user has decided which expense it belongs to.

Authentication and scopes

Requests must include Authorization: Bearer rly_your_key_here. Each endpoint requires:

  • attachments:write — upload, link
  • attachments:read — retrieve metadata + URL

Endpoints

Upload an attachment

POST /api/v1/attachments

Multipart form-data with a single file field. Returns the new attachment id and a URL the file can be fetched from.

Allowed types: image/jpeg, image/png, image/webp, application/pdf. Max upload size 20MB (the server compresses images to WEBP and PDFs via Ghostscript before storing — the stored size will usually be much smaller).

curl -X POST https://accounting.relentify.com/api/v1/attachments \
  -H "Authorization: Bearer rly_your_key_here" \
  -F "[email protected]"

The newly-uploaded attachment is unlinkedrecord_type and record_id are both null until you call link. Unlinked attachments are storage-only; they don't show on any record's detail page.

Get an attachment

GET /api/v1/attachments/:id

Returns the metadata (filename, size, mime type, link state) plus a url field. With the default Postgres storage backend the url is the in-app /api/attachments/:id/file path; with the R2 backend it's a presigned https:// URL good for one hour.

Link an attachment to a record

POST /api/v1/attachments/:id/link

{
  "source_type": "expense",
  "source_id": "0a1b2c3d-..."
}

source_type must be one of: bill, expense, mileage, bank_transaction, comment. record_type / record_id are also accepted as aliases.

Re-linking an already-linked attachment overwrites the previous target. There is no "unlink" — to detach, link to a different record.

Test mode

Test-mode keys validate the request and return a stubbed response with "test": true in the envelope; nothing is uploaded or linked.

Storage backends

The deployment chooses between two storage backends via the STORAGE_BACKEND env var:

  • postgres (default): bytes live in the acc_attachment_data table. The url field returned to API consumers is a relative path — append it to the accounting host to fetch.
  • r2: bytes live in Cloudflare R2; url is a presigned GET URL with a one-hour TTL. The R2 bucket must have Object Read & Write on the API token used by the app.