Uploaded invoices and quotes sort, filter, search, export and bulk-download with the synced ones. Push the same record again to update it, for example when an invoice is paid.
Before you start
- Create an API key. Go to Settings > Integrations, find the API Access card, click Manage API Keys, then Create New Key. Creating keys needs the Integrations permission at Manage. Pick these scopes:
- Write Financial (
financial.write) for invoices and quotes. - Write Documentation (
documentation.write) for orders and other documents. - Read Companies (
companies.read) to look up company IDs. - Optional: Read Financial (
financial.read) to list what you uploaded.
- Write Financial (
- Create the key as an MSP staff user with write access. A key acts as the user who created it. Invoice and quote uploads need Integrations at Write. Document uploads need the Documentation settings permission at Write. Admin roles have both. A key created by a company (portal) user cannot upload.
- Use your region’s address. It is the address you sign in to MSPortal at: US tenants call
https://app.msportal.ai, EU tenantshttps://eu.msportal.ai. The examples use the EU address. - No PSA or quoting integration is needed. The Invoices and Quotes pages appear after your first upload. To let a client’s portal users open them, give their role the Invoices, Quotes or Documentation permission.
- The examples use
curlandjq. Any language that can send JSON over HTTPS works the same way.
"") in a text field is refused with a 400.
Find the company ID
Each record belongs to one company (your client). List your companies once, match them to your clients byname, and store each MSPortal id next to the client in your system:
pagination.hasNextPage is true, fetch the next page (page=2, and so on). Use the matching company’s id as companyId below.
Upload an invoice
Send the invoice and its PDF in one request. The PDF goes inpdf.contentBase64 as standard base64 (no data: prefix), up to 3,000,000 bytes.
201 with the MSPortal id. Store it if you may want to delete the invoice later.
Mark an invoice paid
Send the sameexternalId again with the new values. Leave out pdf to keep the stored PDF, or send a new one to replace it. The answer is 200 with "created": false.
Upload a quote
Push the same
externalId again to change the stage or replace the PDF. As with invoices, every push replaces the quote’s values; a push without quoteDate keeps the stored date.
Upload an order or another document
Orders have no table of their own, so they go into the company’s Documentation library. PDF, DOCX, XLSX, XLS and CSV files up to 50 MiB are accepted. A document upload takes three calls: prepare, send the bytes, finalize.201, and the document appears in the company’s Documentation library.
companyIdis required. A document without a company would be visible to every client, so it is refused.fileSizemust be the exact size in bytes, andfileNamemust end in the file’s extension (.pdf,.docx,.xlsx,.xlsor.csv) and must not contain...- To file orders in their own folder, click New folder on the Documentation page once, read the folder’s
idfromGET /documentation/folders, and pass it asfolderId. A folder without a company works for every client. - If finalize times out, send it again with the same
documentIdand body: it returns the same document, never a duplicate. A retry that names a different company or folder answers409. - An
uploadUrlaccepts one file. If the bytes you sent had the wrong size or type, finalize answers400; start again with a new prepare call. - Documents uploaded this way can’t be deleted through the API yet.
Delete an upload
Delete an invoice or quote you uploaded by its MSPortalid. The PDF is removed with it. Invoices synced from your PSA can’t be deleted this way and answer 404.
Retries
- Invoice and quote uploads are keyed by
externalId, so sending the same request again is always safe: it updates the record instead of creating a second one. - Document uploads are safe to finalize again with the same
documentId. If a prepare call fails, start over with a new prepare.
Errors
Every error answers with a JSON body.error is a readable message, code a stable value to branch on, and requestId identifies the call for support. When fields are invalid, details names each one:
{ "error": "a PDF is required", "code": "VALIDATION_ERROR", "requestId": "…" }.