API reference: customer portal

Every endpoint a customer contact's token can use: everything the portal does.

Every endpoint of the portal half of the API, 92 in all, grouped by area. How to sign in, the envelope every answer uses, paging, errors and permissions are on REST API and webhooks; this page is the list.

A contact token is needed for every address here. It sees the contact’s own company’s records and only the parts of the portal the contact has been given. Paths start /api/v1/portal/.

Invoices and statement

MethodAddressWhat it does
GET/api/v1/portal/invoicesList the company's invoices, newest first. Drafts are never listed.
GET/api/v1/portal/invoices/statementThe company's account as a ledger: opening balance, every invoice, payment, credit and refund in the period, and the closing balance - per currency. Takes period (this_year, last_month.. or custom with from and to), as the statement page does.
GET/api/v1/portal/invoices/{invoice}One invoice, with its lines and the payments made against it.

Estimates, proposals and contracts

MethodAddressWhat it does
GET/api/v1/portal/contractsList the company's contracts, the latest start date first. Contracts hidden from the customer or in the bin are never listed.
GET/api/v1/portal/contracts/{contract}One contract, with its wording, its signatures and who is still to sign.
POST/api/v1/portal/contracts/{contract}/signSign the contract on the customer's side. Takes signature (a PNG data URI of the drawn signature), signed_by (the signer's full name) and accept (1, the signer agreeing to the signing statement).
GET/api/v1/portal/estimate-requestsList the company's estimate requests, newest first.
POST/api/v1/portal/estimate-requestsAsk to be quoted. Takes subject, description, budget, wanted_by, the workspace's extra questions as answers[<id>], and up to five attachments (multipart). The company and contact are the caller's own.
GET/api/v1/portal/estimate-requests/createWhat the request form asks: the workspace's own extra questions (each sent as answers[<id>]) and the upload limits for attachments.
GET/api/v1/portal/estimate-requests/{estimateRequest}One estimate request, with the files shared with the customer and the estimate it led to once that has been sent.
GET/api/v1/portal/estimatesList the company's estimates, newest first. Drafts are never listed. Filter with status (sent, accepted, declined).
GET/api/v1/portal/estimates/{estimate}One estimate, with its lines, totals, signatures and the discussion (never internal comments). Opening it the first time stamps it as viewed, as opening the page does.
POST/api/v1/portal/estimates/{estimate}/acceptAccept the estimate. Takes lines (the ids of the optional lines kept) with lines_submitted=1 when the estimate offers optional lines; an estimate that asks to be signed is accepted by signing it instead.
POST/api/v1/portal/estimates/{estimate}/declineDecline the estimate. Takes an optional decline_reason.
POST/api/v1/portal/estimates/{estimate}/signSign the estimate, which accepts it. Takes signature (a PNG data URI of the drawn signature), signed_by (the signer's full name), accept (1, the signer agreeing to the signing statement) and, as for accept, lines with lines_submitted.
GET/api/v1/portal/pdf/{kind}/{id}Download the PDF of an invoice, estimate, proposal or contract the company has been sent ({kind} is invoice, estimate, proposal or contract). Streams application/pdf as an attachment. Estimates and proposals answer 403 while "Let clients download the PDF" is off.
GET/api/v1/portal/proposalsList the company's proposals, newest first. Drafts are never listed. Filter with status (sent, open, revised, accepted, declined).
GET/api/v1/portal/proposals/{proposal}One proposal, with its content, lines, totals, signatures and the discussion (never internal comments). Opening it the first time stamps it as viewed and moves a "sent" proposal to "open", as the page does.
POST/api/v1/portal/proposals/{proposal}/acceptAccept the proposal. Takes lines (the ids of the optional lines kept) with lines_submitted=1 when it offers optional lines; a proposal that asks to be signed is accepted by signing it instead.
POST/api/v1/portal/proposals/{proposal}/declineDecline the proposal. Takes an optional decline_reason.
POST/api/v1/portal/proposals/{proposal}/signSign the proposal, which accepts it. Takes signature (a PNG data URI of the drawn signature), signed_by (the signer's full name), accept (1, the signer agreeing to the signing statement) and, as for accept, lines with lines_submitted.
POST/api/v1/portal/sales-documents/thread-orderRemember which way round this contact reads a discussion. Takes order: "oldest" (the default) or "newest".
POST/api/v1/portal/sales-documents/{kind}/{document}/commentsPost a comment to the discussion on an estimate or a proposal ({kind} is "estimate" or "proposal"). Takes body and up to five attachments (multipart). Answers 404 while the discussion is switched off.

Projects

MethodAddressWhat it does
GET/api/v1/portal/projectsList the company's projects: live work first, then by deadline. Each carries only what its switches allow (deadline with view_overview, progress with view_progress) and the tabs it has.
GET/api/v1/portal/projects/requestWhether this contact may ask for a new project (404 when the workspace does not let customers request projects).
POST/api/v1/portal/projects/requestAsk for a new project. Takes name, description and deadline; it is created "not started", for the company, for staff to take up. 403 when the workspace does not let customers request projects.
GET/api/v1/portal/projects/{project}One project: its headline, the tabs it shows this customer, its switches, and its overview when the overview tab is one of them.
POST/api/v1/portal/projects/{project}/feedbackRate the project. Takes rating (1-5) and an optional comment; leaving feedback again changes the first, until staff have replied to it. 403 while feedback is closed for this project.
POST/api/v1/portal/projects/{project}/filesUpload files to the project. Takes files[] (one to ten, multipart); they are visible to the company's contacts at once and the project's team is told.
GET/api/v1/portal/projects/{project}/files/{file}Download a project file under its own name.
DELETE/api/v1/portal/projects/{project}/files/{file}Delete a file the contact uploaded themselves.
POST/api/v1/portal/projects/{project}/files/{file}/commentsComment on a project file. Takes body.
POST/api/v1/portal/projects/{project}/tasksAdd a task to the project. Takes title, description, start_date, due_date and milestone_id (one of the project's). It starts in the first open status, visible to the customer; the project's team is told.
GET/api/v1/portal/projects/{project}/tasks/createWhat the new-task form offers: the project's milestones a task can be put under (empty unless the customer may see milestones). 404 unless the customer may add tasks to this project.
GET/api/v1/portal/projects/{project}/tasks/{task}One task: its details, and - as the project's switches allow - its assignees, checklist, time logged and comment thread (never internal comments; files only those shared with the customer).
PUT / PATCH/api/v1/portal/projects/{project}/tasks/{task}Change a task the contact added themselves. Takes title, description, start_date, due_date and milestone_id; any left out keeps its current value. 403 for anybody else's task or while edit_own_tasks is off.
POST/api/v1/portal/projects/{project}/tasks/{task}/commentsComment on a task. Takes body and, while the project allows attaching files, up to five attachments (multipart).
DELETE/api/v1/portal/projects/{project}/tasks/{task}/comments/{comment}Delete a comment the contact wrote on a task - only their own, and only while they may still comment.
GET/api/v1/portal/projects/{project}/{tab}One tab of a project: overview, tasks, milestones, gantt, files, activity or feedback. 404 for a tab this project does not show the customer.

Support tickets

MethodAddressWhat it does
GET/api/v1/portal/ticketsList the company's tickets, the most recently answered first. Merged and archived tickets are left out, as on the page. Filter with status (a ticket status id).
POST/api/v1/portal/ticketsOpen a ticket. Takes subject, message, priority (a slug from the form options), ticket_department_id and up to five attachments (multipart). The company and contact are the caller's own.
GET/api/v1/portal/tickets/createWhat the new-ticket form offers: the departments, the priorities (send the slug) and the upload limits.
GET/api/v1/portal/tickets/{ticket}One ticket, with its public replies (never private notes), the files sent to the customer, and the statuses the customer may move it to.
DELETE/api/v1/portal/tickets/{ticket}/attachments/{attachment}Remove a file the contact attached to one of their replies, until staff have answered after it.
POST/api/v1/portal/tickets/{ticket}/replyReply to a ticket. Takes message, up to five attachments and an optional voice_note (multipart); a voice note can be the whole reply.
POST/api/v1/portal/tickets/{ticket}/statusMark the ticket resolved, or reopen it. Takes ticket_status_id - one of the status_choices the ticket offers; any other answers 403.

Account, messages, notifications and privacy

MethodAddressWhat it does
GET/api/v1/portalThe contact's overview: each panel's key, label, size and data, in the order the business (and, where allowed, the contact) arranged them, and which shortcuts the contact is offered.
GET/api/v1/portal/accountThe signed-in contact's own details, the tabs of My account the business shows them, and the parts of the portal they may open.
PUT / PATCH/api/v1/portal/accountChange the contact's own name, phone number and job title. Takes first_name, last_name, phone and job_title; any left out keeps its current value. The email address cannot be changed from the portal.
GET/api/v1/portal/account/companyThe company's billing and shipping addresses, and whether this contact may change them (primary contacts only). Answers 404 while the business hides the Company tab.
PUT / PATCH/api/v1/portal/account/companyChange the company's billing and shipping addresses - primary contacts only. Takes address, city, state, zip, country and the same five with a shipping_ prefix; nothing else about the company can be changed here.
GET/api/v1/portal/account/contactsEverybody at the contact's company, primary contacts first, each with the parts of the portal they may open. Primary contacts only.
POST/api/v1/portal/account/contactsAdd a contact to the company and email them a link to choose a password. Takes first_name and email (required), last_name, phone, job_title, and permissions - the area keys they may open (see GET account/contacts/create). Primary contacts only.
GET/api/v1/portal/account/contacts/createThe parts of the portal a contact can be given (less modules switched off), for the add and edit forms - each a key for "permissions" and its label. Primary contacts only.
GET/api/v1/portal/account/contacts/{colleague}One contact at the company, with the parts of the portal they may open. Primary contacts only.
PUT / PATCH/api/v1/portal/account/contacts/{colleague}Change a contact's name, phone, job title and - unless they are a primary contact, who always sees everything - the parts of the portal they may open. Takes first_name (required), last_name, phone, job_title and permissions. Any field left out keeps its current value; permissions, when sent, replaces the whole list. Primary contacts only.
POST/api/v1/portal/account/contacts/{colleague}/deactivateStop a contact signing in. Not yourself, and not the company's last active primary contact. Primary contacts only.
POST/api/v1/portal/account/contacts/{colleague}/inviteEmail a contact who has not chosen a password yet a new link to choose one; the new link replaces the old. Primary contacts only.
POST/api/v1/portal/account/contacts/{colleague}/reactivateLet a switched-off contact sign in again. Primary contacts only.
PUT / PATCH/api/v1/portal/account/passwordChange the contact's password. Takes current_password, password and password_confirmation; the contact is emailed that it changed. Answers 404 while the business hides the Password tab.
GET/api/v1/portal/customise/dashboardThe overview panels this contact may arrange, in their order, each with its key, label, icon and whether they have hidden it; and whether they have an arrangement of their own.
PUT / PATCH/api/v1/portal/customise/dashboardSave the contact's arrangement of the overview. Takes widgets: an object of panel key => {"visible": true|false}, in the order to show them. Keys the contact was not offered are ignored.
POST/api/v1/portal/customise/dashboard/resetPut the overview back to the business's standard layout.
GET/api/v1/portal/customise/menuThe menu entries this contact may arrange, in their order, each with its key, label, icon, whether they have hidden it and whether it is locked (always shown); and whether they have an arrangement of their own.
PUT / PATCH/api/v1/portal/customise/menuSave the contact's arrangement of the menu. Takes items: an object of entry key => {"visible": true|false}, in the order to show them. A locked entry cannot be hidden.
POST/api/v1/portal/customise/menu/resetPut the menu back to the business's standard layout.
GET/api/v1/portal/detailsThe contact's own details, while privacy is switched on. The portal's old "My details" page now goes to My account, so there is no page to read them from; the same 404 applies while privacy is off.
PUT / PATCH/api/v1/portal/detailsCorrect the contact's own name and phone number. Takes first_name, last_name and phone; any left out keeps its current value.
GET/api/v1/portal/messagesThe contact's conversations, the latest activity first, each with the other people in it and how many messages are new to the contact.
POST/api/v1/portal/messagesStart a conversation. Takes subject, body, recipients (user ids from GET messages/create - one not on that list refuses the whole message) and up to five attachments[] as multipart files.
GET/api/v1/portal/messages/createWho the contact may write to - staff and colleagues, each {id, name, job_title} - whether they may start a conversation at all, and whether they have asked to be emailed when a message arrives.
POST/api/v1/portal/messages/preferencesChoose whether the contact is emailed when a message arrives. Takes emails (boolean); left out, the choice stays as it is.
GET/api/v1/portal/messages/{conversation}One conversation with every message in it, oldest first, and whether the contact may reply. Reading it marks it read for the contact.
POST/api/v1/portal/messages/{conversation}/replyReply to a conversation. Takes body and up to five attachments[] as multipart files. Refused with 403 when replies to it are switched off.
GET/api/v1/portal/notificationsThe contact's notices, newest first. Takes unread=1 for only those not yet read.
GET/api/v1/portal/notifications/feedThe latest ten notices and the number unread - what the bell polls. Takes after (a notice id) for only those newer than it.
POST/api/v1/portal/notifications/read-allMark every notice read.
POST/api/v1/portal/notifications/{id}/readMark one notice read, and say how many are still unread.
GET/api/v1/portal/pages/{slug}One published customer page by its slug: title and content (HTML, sanitised as the portal shows it).
GET/api/v1/portal/privacyThe contact's privacy page: which parts are offered, the consents asked for and their answers, the history of their answers, their latest data export and removal request, how long an export link lasts, and the privacy policy and terms links.
POST/api/v1/portal/privacy/consentsRecord the contact's answers to every consent currently asked for. Takes purposes: the ids of those they agree to; every other active one is recorded as withdrawn. Left out altogether, every answer stands.
POST/api/v1/portal/privacy/exportBuild a copy of everything held about the contact, now, replacing any copy still waiting. Download it from the id returned, at GET privacy/export/{privacyRequest}, before it expires.
GET/api/v1/portal/privacy/export/{privacyRequest}Download the contact's data export as a zip file. It is deleted from the server once fetched, so this works once; afterwards, and once the link has expired, it answers 404.
POST/api/v1/portal/privacy/removalAsk the business to remove the contact's details. Takes confirm (must be accepted) and an optional message. Refused with 422 while an earlier request is still waiting.

Signing in

MethodAddressWhat it does
POST/api/v1/portal/ai/actions/{action}/cancelCancel an action the assistant proposed. Nothing happens.
POST/api/v1/portal/ai/actions/{action}/confirmCarry out an action the assistant proposed, as the contact, through the portal's own rules. Takes inputs (the values for the boxes the card asks for, such as a password) and typed (what its confirm_text asks to be typed first). An action already dealt with, or one that fails, is 422 and stays as it was.
GET/api/v1/portal/ai/assistantWhat the assistant's opening screen offers: whether it is available, the month's allowance and the quick starters this contact may use. 403 while the business has not switched the portal's assistant on.
POST/api/v1/portal/ai/assistantAsk the assistant something, starting a conversation when none is named. Takes question (at most 4000 characters) and conversation (the id of one of the contact's conversations to carry on). Answers with the conversation id, its title, the answer as text and as HTML, and any actions it proposes - nothing it proposes happens until it is confirmed. A failed answer is 422 with the conversation id in data, because the question was recorded anyway.
GET/api/v1/portal/ai/assistant/historyThe contact's thirty most recent conversations, newest first.
GET/api/v1/portal/ai/assistant/{conversation}One of the contact's conversations with every message in it, and the actions the assistant proposed along the way with their status.
DELETE/api/v1/portal/ai/assistant/{conversation}Delete one of the contact's conversations.
POST/api/v1/portal/auth/tokenExchange a customer contact's email and password for a portal token.