Skip to content

Customer & vendor portal

The portal is how a contact sees their own side of your books without you sending anything by hand. You issue a private link from the contact record; they open it in a browser and get their documents. There is no account to create, no password, and nothing for them to edit — the portal only ever shows.

One link covers both relationships. A contact you sell to sees invoices and a statement; a contact you buy from sees bills, purchase orders and the payments you have made them; a company that is both sees both, from the same link.

A portal link carries an opaque, randomly generated token, and the token is the scope. When a request arrives, the server hashes the token, looks for a live access row with that hash, and takes one fact from it: which contact this is. Every query the portal runs from then on is filtered by that contact.

There is no contact parameter anywhere in the portal for a caller to set, so there is nothing to tamper with. Asking for a specific document by its own id still requires that the document belong to the token’s contact — and one that does not comes back as not found, never as “forbidden”, so the portal will not even confirm that someone else’s invoice exists.

Two more properties are worth knowing before you hand a link out:

  • The token is stored only as a hash. The plaintext is shown once, when the link is generated, and is never written down anywhere you or anyone else can read it back. A stolen copy of the database yields no working links.
  • A dead link says nothing about why. Revoked, expired, never valid or simply mistyped all produce the same message — “This link is invalid or has expired.” Distinguishing “expired” from “never existed” would itself be information worth having to anyone guessing at tokens.

A portal token is not a login. It carries no user, no role and no reach into the rest of Accounts; presenting it to the ordinary API authenticates nothing. It is the one part of the API that sits outside the app’s sign-in and permission gate, deliberately, because its visitor is not a user of your app — the token check is the whole boundary, which is why it does so little.

In the web app, portal access hangs off the contacts list: the row action Portal, offered on any contact flagged as a customer or a vendor. It opens a dialog with one setting — how many days the link should last, from 1 to 3650, or blank for a link that never expires — and a Generate link button. On success the full link appears with a Copy button and a notice that this is the only time you will see it.

That notice is literal. Close the dialog without copying and the link is gone; no screen in the product can retrieve it. The way back is to generate another, which rotates: a contact has exactly one working link at a time, and minting a new one kills the previous one on the spot.

Revoke portal access sits in the same dialog behind a confirm step, and tells you afterwards whether there was a live link to kill. Revocation takes effect immediately — anyone still holding the link gets the invalid-or-expired message on their next click.

On Android the same two actions live on the contact detail screen, in a Portal access card, with the lifetime offered as chips — 30 days, 90 days, 1 year or no expiry — and Share portal link handing the freshly minted link straight to the system share sheet, so it can go out over WhatsApp, SMS or mail without ever being pasted anywhere. Revoke access is there too, behind a confirmation.

Both actions are contact writes, so they need contacts:write. Without it the web row action is hidden and the Android card explains which permission is missing rather than disappearing. Issuing and revoking are both recorded in the audit trail as a change to the contact.

Whatever you send them. The product does not email portal links — it produces the link and leaves delivery to you, which is why the web dialog offers a copy button and the Android screen a share sheet.

The link points at your own app: …/portal?token=…, on the same address staff sign in at unless your deployment has been configured to serve the portal from a different one. Because the token travels in the URL, the link is the credential — anyone who has it can see that contact’s account. Treat it accordingly: send it to the person who should have it, prefer a link with an expiry over one without, and revoke when the people at a customer or supplier change.

One page, no navigation, no sign-in — the application name your deployment is configured with at the top, and their own name and account underneath. That header name is a build-time setting, not per-tenant branding: on a deployment shared between tenants every portal shows the same words, above the subtitle Customer portal. Sections appear only when there is something in them, so a customer never sees empty vendor tables and a supplier never sees an empty receivables balance.

On the customer side: their outstanding balance, a Download statement button, and a table of their invoices — number, date, due date, status, total and balance — with a PDF download on every row. The statement is the same document your own staff would send them, covering all activity up to today; there is no date picker on the portal, and the figures follow the same rules as elsewhere (labelled INR, not currency-converted — see Delivery). Invoice rows do not expand on the portal; the line detail is in the PDF.

On the vendor side: Your bills and Your purchase orders, each row opening to show its lines, taxes and totals, and Payments received, listing the payments you have actually made them. There is no vendor equivalent of the customer statement — a bill carries its own balance due, but there is no running supplier account or downloadable vendor statement.

Only documents you have actually issued are ever returned:

Document Appears on the portal once it is
Invoice SENT, VIEWED, PARTIALLY_PAID, PAID or OVERDUE
Bill OPEN, PARTIALLY_PAID, PAID or OVERDUE
Purchase order SENT, PARTIALLY_BILLED, BILLED or CLOSED
Payment to a vendor COMPLETED

Drafts, cancellations and voids are never visible, and neither is a purchase order that is still in approval or approved but not yet sent. The filter is applied on every read, so moving a document back out of those statuses removes it from the portal at once — there is nothing to un-publish.

Every route the portal exposes is a GET. There is no create, update or delete endpoint under it at all, so an attempted write is not something a permission check refuses — there is no handler for it to reach. A portal visitor cannot edit a document, dispute one, upload anything, or change their own details.

That includes paying. There is no Pay button and no payment journey behind the portal; a customer who wants to settle an invoice does it the way they do today and you record the payment in receivables.

The read side has one gap worth knowing about: nothing in the product tells you whether a given contact currently has a live link, or when it was last opened. The server does record the last time each link was used, but there is no screen or endpoint that reads it back. In practice you manage links by action rather than by status — generate one when a contact needs access, revoke when they should not have it, and generate a fresh one whenever you are unsure.

  • Give a customer or supplier a private link to their own account, with an expiry or without one, from the web app or from Android.
  • Let a customer pull their own invoice PDFs and statement instead of asking you for them.
  • Show a supplier the bills you have on file, the orders you have placed, and what you have paid them.
  • Rotate a link that has gone astray, or revoke it outright, with immediate effect.