Attachments
Attach supporting files to an invoice or an estimate: a timesheet, a purchase order, a signed scope of work, a receipt. Each file is either shared, which puts it on the invoice page your customer opens, or internal, which only you and your team can see.
Attachments are switched on by Plirin for your account. If you do not see a Files section on an invoice or estimate, the feature is not available to you yet.
What you can attach
Section titled “What you can attach”- Types: PDF, PNG, JPEG, WebP, HEIC, CSV, plain text (TXT), Word (
.docx) and Excel (.xlsx). - Size: up to 20 MB per file.
- Count: up to 10 files per invoice or estimate.
Open an invoice or estimate and find the Files section. Drag files onto it, or choose Add files. You can add several at once, and each one shows its own progress. Anyone who can manage invoices (the invoice.manage permission) can add, share and remove files. Team members without it can see the list and download files, and each file shows Shared or Internal.
You can attach files to an invoice at any status, including after it has been sent or paid.
Storage on your plan
Section titled “Storage on your plan”Every plan includes file storage, counted across all your invoices and estimates.
| Plan | Storage |
|---|---|
| Starter | 250 MB |
| Growth | 5 GB |
| Scale | 25 GB |
The Files section shows how much you have used, for example “Using 12 MB of 250 MB”.
When storage is full, new uploads are refused with “Your plan’s file storage is full. Remove files or upgrade your plan.” Files you already have stay where they are, and your customers can still open the ones you shared. Removing a file gives its space back straight away.
Storage is shared between test mode and live mode. A file you attach while testing uses real storage, so it counts toward the same total as your live files. Remove test files you no longer need.
Sharing with customers
Section titled “Sharing with customers”A file you add to an invoice is shared by default, and a file you add to an estimate starts internal (see Estimates). Each file has its own switch labelled Visible to customer. Turn it off and the file becomes internal: it stays on the invoice for your team but is not on the customer’s page. You can flip the switch at any time, and it takes effect immediately.
What customers see
Section titled “What customers see”Shared files are listed on the hosted invoice page with their name and size. Selecting one downloads it. A file is never shown for a draft, because a draft has no customer link yet. Files you switch to internal are never listed.
Downloads use a short-lived link, so a copy of the link stops working after about a minute. The customer opens the file again from the page whenever they need it.
Emails
Section titled “Emails”The invoice email, and every reminder and overdue email after it, list the names of shared files and link to the invoice page. The list is read when each email is sent, so a file you removed or switched to internal since the last email is not listed. No email ever attaches them. A file in a customer’s inbox cannot be taken back, while a file on the invoice page can be: if you remove a file or switch it to internal, it is gone for the customer too.
The invoice PDF is still attached to the email as before. Your own attachments are separate from it.
Estimates
Section titled “Estimates”You can attach files to an estimate while you prepare it. Estimate files never appear on your customer’s estimate page.
When an estimate is accepted or you use Convert now, its files are copied onto the new draft invoice, with the same shared or internal setting. The copies count toward your storage, and they are independent: removing a file from the invoice does not remove it from the estimate, or the reverse.
Conversion never fails because of files. If your storage cannot hold every file, the conversion goes ahead and the files that did not fit are left on the estimate. The estimate’s audit entry records how many files were copied and how many were not, so you can attach the missing ones to the invoice by hand after freeing some space.
Removing files
Section titled “Removing files”Choose the bin icon next to a file and confirm. The file is no longer visible to you or your customer, and its storage is released at once. The stored copy is deleted shortly afterwards. Removing a file cannot be undone: attach it again if you need it back.
Adding, sharing changes and removals are recorded in the audit trail as invoice.attachment_added, invoice.attachment_sharing_changed and invoice.attachment_deleted.
Test mode
Section titled “Test mode”Files follow the document they are attached to. A file on a test invoice or estimate is a test file: it appears only while you are in test mode, and is never shown in live mode, whatever you share. A live file is invisible in test mode in the same way. A test API key sees only test files, and a live key only live ones. See Test and Live Mode.
Test files use the same storage allowance as live ones, as noted above.
Why a file is refused
Section titled “Why a file is refused”Plirin checks what a file actually is, not only what its name or the browser says. A refusal reads “That file doesn’t look like the type it claims to be, so it wasn’t added.” Common reasons:
- The contents do not match the type. A program renamed to
invoice.pdfis not a PDF, so it is refused. The same applies to a Word or Excel file that is not really one. - Macros are not allowed. Word and Excel files that contain macros or other embedded code are refused, and so are the macro-enabled formats (
.docm,.xlsm). Save the document as a plain.docxor.xlsxand upload that. - The type is not supported. Anything outside the list above, such as ZIP, SVG, HTML or video, is refused with “That file type isn’t supported. Use PDF, an image, CSV, TXT, Word or Excel.”
- It is too large or the document is full. “Files can be up to 20 MB.” and “This document already has 10 files. Remove one to add another.”
- The upload did not finish. A dropped connection shows “The upload didn’t finish. Please try again.” Nothing is kept, so uploading again is safe.
Files are renamed to match what they are. The name you see always ends in an extension that fits the verified type. A file called run.bat that is uploaded as plain text is stored as run.bat.txt, and a PDF called statement becomes statement.pdf.
Plirin does not scan files for viruses, and it never opens or runs them: files are only ever offered as downloads. Treat attachments you receive from others with the care you would give any download.
Using the API
Section titled “Using the API”The REST API manages attachments with the same rules as the dashboard: the same types, sizes, counts, storage limit and checks. Requests need an API key (Authorization: Bearer sk_...). A test key only sees test documents and test files, and a live key only live ones. A document or file from the other environment, or from another merchant, answers 404.
Uploading is three steps: ask for an upload, send the bytes to storage, then confirm. The SDKs do all three in one upload call.
| Method and path | What it does |
|---|---|
POST /api/v1/invoices/{invoice_id}/attachments | Start an upload on an invoice |
POST /api/v1/estimates/{estimate_id}/attachments | Start an upload on an estimate |
POST /api/v1/attachments/{attachment_id}/confirm | Finish an upload. The file is checked and becomes visible |
GET /api/v1/invoices/{invoice_id}/attachments | List an invoice’s files |
GET /api/v1/estimates/{estimate_id}/attachments | List an estimate’s files |
PATCH /api/v1/attachments/{attachment_id} | Share a file with the customer or make it internal |
DELETE /api/v1/attachments/{attachment_id} | Remove a file |
GET /api/v1/attachments/{attachment_id}/download | Redirect (302) to a short-lived download link |
Start an upload
Section titled “Start an upload”POST /api/v1/invoices/{invoice_id}/attachments
{ "file_name": "scope-of-work.pdf", "content_type": "application/pdf", "size_bytes": 482113 }content_type must be one of the supported types, and size_bytes the file’s size in bytes (the upload is refused if it turns out larger). The response is 201:
{ "attachment": { "id": "6f0c7f0e-9d2b-4b0e-8a47-3b3c1f6f3a10", "file_name": "scope-of-work.pdf", "content_type": "application/pdf", "size_bytes": 482113, "shared_with_customer": true, "created_at": "2026-10-03T09:30:00+00:00", "livemode": true }, "upload": { "url": "https://...", "fields": { "key": "...", "Content-Type": "application/pdf" } }, "expires_at": "2026-10-03T09:35:00+00:00"}Send the file with a multipart/form-data POST to upload.url: every entry of upload.fields first, in order, then the file as the last part named file. Do not send your API key to this address. The form is valid for 5 minutes (expires_at), and its Content-Type and size limit must match what you declared.
A file that is never confirmed is never listed and does not use storage. An abandoned upload is removed after about an hour (up to two), and until then it holds one of the document’s 10 slots.
Confirm
Section titled “Confirm”POST /api/v1/attachments/{attachment_id}/confirmPlirin reads the stored file, checks its contents and size, and charges your storage. The response is the attachment object above. Confirming a file that is already confirmed returns it unchanged. If the check fails the file is discarded and nothing is charged.
List, share, remove, download
Section titled “List, share, remove, download”GET .../attachments returns { "data": [ ...attachment objects... ] }, oldest first, confirmed files only.
PATCH /api/v1/attachments/{attachment_id} takes { "shared": true } or { "shared": false } and returns the updated attachment.
DELETE /api/v1/attachments/{attachment_id} returns { "deleted": true }.
GET /api/v1/attachments/{attachment_id}/download answers 302 with the link in Location. The link works for about a minute and forces a download.
Errors
Section titled “Errors”Errors use the standard envelope, { "error": { "type", "code", "message", "param" } }. For a 400, param carries the specific attachment code.
| Status | code | param | When |
|---|---|---|---|
400 | invalid_request | ATTACHMENT_TYPE_NOT_ALLOWED | The content type is not supported |
400 | invalid_request | ATTACHMENT_TOO_LARGE | size_bytes is zero or over 20 MB |
400 | invalid_request | ATTACHMENT_LIMIT_REACHED | The document already has 10 files |
400 | invalid_request | ATTACHMENT_UPLOAD_MISSING | Confirm was called before the bytes reached storage. Upload again and retry |
400 | invalid_request | ATTACHMENT_SIZE_MISMATCH, ATTACHMENT_CONTENT_MISMATCH, ATTACHMENT_CHANGED | The stored file is larger than declared, is not what its type claims, or changed during the check |
402 | plan_limit_exceeded | none | Your plan’s storage is full |
403 | permission_denied | none | Attachments are not switched on for the platform, or file storage is not configured |
404 | resource_not_found | none | The document or file does not exist, belongs to another merchant, or is in the other environment |
503 | internal_error | STORAGE_UNAVAILABLE | File storage is temporarily unavailable, or its credentials are not configured. Retry shortly |
The same codes appear in GraphQL Mutations for the dashboard API.