Manage customers
[admin] [billing]
Customer management in the admin: invite, activate, assign packages, contract data, cancellation, deletion. Including a GDPR-compliant archive.
Who it is for
Admins who onboard their license customers manually or via a self-service link, assign packages and manage contracts.
Overview
Sidebar → Customers: list of all customers. Columns: customer number, name/company, package, status, created. Two filter rows:
- Status filter: all / active / pending / waiting for payment / paused / inactive / cancelled.
- Industry filter (NACE sections): show only customers whose industry falls into one of the selected NACE sections (multi-select, OR). Empty = all. The customer detail shows each customer's assigned industry.
Status lifecycle
- pending: invited / registered, not yet activated
- pending_payment (waiting for payment): activated by the admin — an invoice has been issued and is awaiting payment. Becomes
activeautomatically when the payment arrives (invoice.paid). - active: activated and paid, has portal access
- paused: temporarily suspended (can be reactivated)
- inactive: deactivated
- cancelled: subscription cancelled (end date reached / set)
- Deleted (
deleted_customersarchive): GDPR-compliant anonymized
Step by step: inviting customers
1. + Invite customer
Customers → + Invite customer.
2. Fields
- Email (required): recipient of the invitation mail
- First name / last name / company
- Package to select (dropdown of all active packages of the provider)
- Add-ons optionally assigned
- Notes (internal, for you)
3. Send
licensio generates a registration token and sends a mail with the link /<tenant>.licensio.io/register/<token>. The customer clicks the link and sets a password — the account is created with status pending.
4. Activate (always via invoice)
You activate the customer manually in the customer detail (Status section → Activate). Activation for a paid, bookable package always issues a Stripe invoice (send_invoice, 14-day due date) on the provider's connected account — regardless of whether a payment method is stored. There is no checkout on the admin path (checkout only happens at self-service /join). The customer moves to status pending_payment and receives the invoice email with a hosted payment link. The status flips to active automatically only when invoice.paid arrives.
- Resend payment link: if the invoice/subscription expired or was cancelled ("dead" subscription), the customer detail shows a "Resend payment link" button that issues a fresh invoice.
- Free / no package: a package with price 0 (or no package) activates directly, no invoice.
- A future contract start is not supported in the invoice flow (blocked with a notice).
- Self-service auto-activate: a package with
auto_activate=trueactivates the customer directly on self-service registration at/join(that path uses checkout and immediate payment), bypassing manual activation.
Customer detail
Click a customer in the list. The layout consists of one fixed header tile and collapsible sections (collapsibles). The open/closed state of each section and the active logs tab are stored per customer in the browser (localStorage). By default all collapsibles are collapsed.
Fixed tile "Contact & subscription"
Always open, at the top. Bundles master data + subscription. Via the Edit button (top right) the master data becomes inline editable (fields turn into inputs, save / cancel):
- Name, company, email, customer number (
customer_number, e.g. PT-2026-0001), status badge, external ID - Address (street, zip, city, country), VAT ID, tax number
- Contact person, payment method, internal no. (
customer_number_int, secondary) - Industry (NACE): the customer's assigned NACE Rev. 2.1 divisions (up to 3, the first is the primary industry), shown as readable labels (
customers.industry_codes, migration 093) - Account type:
Whitelabel(default) orConnect, derived fromcustomers.connect_user_id(NULL = Whitelabel, set = Connect) - Payment status (Stripe subscription live: status, invoice status such as "paid" / "open until <date>" / "cancelled", amount, next/last charge, link to the hosted invoice)
- Invoices: the subscription's Stripe invoices (number, amount, status, link), so proration invoices for add-on packages are visible without a Stripe login
- Packages & payment: base package (switchable) + add-ons, add-package button (create payment link / send by mail)
Edit (admin): PATCH /api/customers/[id] writes first_name, last_name, company, responsible_person, street, zip, city, country, vat_id, tax_number, payment_method. Best-effort Stripe sync as in the portal self-edit: address + name onto the Stripe customer, vat_id as an eu_vat tax ID. tax_number stays only in the DB (Stripe does not know national tax numbers). country is validated as a 2-letter ISO code. Email stays excluded (its own OTP flow via "Change email").
Collapsible sections (default: collapsed)
- Logs: tab bar with three tabs (each the last 10):
- File downloads: from
download_logs(date, product, file type) - Logins: from
customer_activity(activity_type='login', date + truncated IP hash) - Feed exports: from
customer_activity(activity_type='feed_export', date + format + via token/session) - An empty tab shows "No data yet". The tab selection is remembered per customer.
- File downloads: from
- Support history: last 3 tickets (
support_tickets, sorted byupdated_at, with a status badge + link to the ticket detail page) + email history (email_logs) - Consents: GDPR proof (
customer_consents, only if present) - Documents: contract uploads (
customer_documents) + contract start date - Internal notes
- Status: activate (pending → pending_payment via invoice → active on
invoice.paid; "Resend payment link" while pending_payment) / deactivate / reject activation (rejected_at,rejection_reason) / pause-reactivate. Cancellation is integrated here (only withstatus='active'): "Cancel subscription" (date modal) or withdraw cancellation. - Actions: resend registration link, password reset, open Stripe billing portal
- Danger zone: Delete (GDPR Art. 17): the customer is archived in
deleted_customers(onlycustomer_number,tenant_id,deleted_at,deleted_by, NO PII), the auth user + customer record are deleted.
Note: logins + feed exports come from the customer activity logging (migration 060). Downloads stay separate in
download_logs. The account type anchorcustomers.connect_user_id(migration 061, nullable, still without FK): licensio Connect is Phase 2.
Customer numbers
An auto trigger generates: PT-2026-0001, PT-2026-0002, ... per provider + year. After a customer is deleted the number is NOT reused: it stays reserved in the deleted_customers archive (migration 013).
Frequently asked questions
How long does the Stripe synchronization take?
Immediately on save. With Stripe Express accounts some fields (e.g. company.address) are not accepted: then it is silent best-effort and the rest is saved.
Where does the customer address come from?
At the self-service /join checkout Stripe asks for a mandatory address (and optionally the VAT ID). After completion the webhook (checkout.session.completed) takes over street/zip/city + VAT ID automatically into the customer record, but only if the fields are still empty. The admin activation path issues a send_invoice invoice (no checkout), so there the address is not collected automatically — maintain it in the admin or let the customer fill it in the portal. An address maintained later in the portal or admin is never overwritten. country is not taken over from the checkout (it stays at the registration value). Note: the setup checkout (packages without an immediate charge) collects no address, so there the field stays empty until it is maintained in the portal/admin. Existing customers can be backfilled via a backfill script (apps/portal/scripts/backfill-stripe-addresses.ts).
What if a customer registered themselves?
Self-service packages create the customer with status='pending'. You see them in the list with the "Pending" filter.
Can I import customers via CSV?
Phase 2: bulk import is not yet implemented (the product_imports table is a skeleton for it).
GDPR data access for a customer? Currently exportable manually from the DB. Self-service data access (Phase 2 / nice-to-have).
What if the Stripe customer update fails? The licensio customer is saved anyway. You see a warning in the UI. You can retry the Stripe sync via the "Sync again" button.