diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts
index b6e587ca..97980ec1 100644
--- a/lib/api/schemas.ts
+++ b/lib/api/schemas.ts
@@ -601,6 +601,14 @@ export const MarkInvoicePaidSchema = z.object({
export const CreateCustomerSchema = z.object({
name: z.string().min(1, 'Customer name is required'),
customer_type: CustomerTypeSchema,
+ // Kundnummer shown on invoices. Free text, not unique in v1. Empty string
+ // and null both clear the value (routes normalize '' to null).
+ customer_number: z
+ .string()
+ .trim()
+ .max(32, 'Customer number must be 32 characters or fewer')
+ .nullable()
+ .optional(),
email: z.string().email('Invalid email address').optional(),
phone: z.string().optional(),
address_line1: z.string().optional(),
diff --git a/lib/invoices/pdf-template.tsx b/lib/invoices/pdf-template.tsx
index c1cdc0e5..acd4e2b0 100644
--- a/lib/invoices/pdf-template.tsx
+++ b/lib/invoices/pdf-template.tsx
@@ -42,6 +42,7 @@ const LABELS = {
yourReference: 'Er referens:',
ourReference: 'Vår referens:',
// Customer box
+ custNo: 'Kundnr:',
orgNo: 'Org.nr:',
vat: 'VAT:',
// Table columns
@@ -111,6 +112,7 @@ const LABELS = {
deliveryDate: 'Delivery date:',
yourReference: 'Your reference:',
ourReference: 'Our reference:',
+ custNo: 'Customer no.:',
orgNo: 'Reg. no.:',
vat: 'VAT:',
colDescription: 'Description',
@@ -799,6 +801,12 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{customer.country && customer.country !== 'SE' && (
{customer.country}
)}
+ {/* Seller-assigned kundnummer: no per-customer-type guard needed,
+ it identifies the customer in the seller's own register and
+ carries no personal data of its own. */}
+ {customer.customer_number && (
+ {L.custNo} {customer.customer_number}
+ )}
{/* Suppress the identifier row for private customers: their
personnummer is not required on a B2C invoice (ML 17 kap 24§
asks for name + address only) and printing it is a GDPR
diff --git a/messages/en.json b/messages/en.json
index 3d9b9505..1d81ed3b 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -660,6 +660,7 @@
"no_contact_info": "No contact details",
"no_business_info": "No business details",
"no_invoices": "No invoices linked to this customer",
+ "label_customer_number": "Customer number:",
"label_org_number": "Org. no.:",
"label_vat": "VAT:",
"label_payment_terms": "Payment terms:",
@@ -695,6 +696,10 @@
"name_label": "Name *",
"name_placeholder": "Company or person name",
"name_required": "Name is required",
+ "customer_number_label": "Customer number",
+ "customer_number_placeholder": "E.g. 1001",
+ "customer_number_hint": "Shown on the invoice. Leave empty if you do not use customer numbers.",
+ "customer_number_too_long": "Customer number must be 32 characters or fewer",
"email_label": "Email",
"email_placeholder": "name@company.com",
"email_invalid": "Invalid email address",
diff --git a/messages/sv.json b/messages/sv.json
index cac13a5e..db4f5fc7 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -660,6 +660,7 @@
"no_contact_info": "Inga kontaktuppgifter",
"no_business_info": "Inga företagsuppgifter",
"no_invoices": "Inga fakturor kopplade till denna kund",
+ "label_customer_number": "Kundnummer:",
"label_org_number": "Org.nr:",
"label_vat": "VAT:",
"label_payment_terms": "Betalningsvillkor:",
@@ -695,6 +696,10 @@
"name_label": "Namn *",
"name_placeholder": "Företagsnamn eller personnamn",
"name_required": "Namn krävs",
+ "customer_number_label": "Kundnummer",
+ "customer_number_placeholder": "T.ex. 1001",
+ "customer_number_hint": "Visas på fakturan. Lämna tomt om du inte använder kundnummer.",
+ "customer_number_too_long": "Kundnumret får vara högst 32 tecken",
"email_label": "E-post",
"email_placeholder": "namn@foretag.se",
"email_invalid": "Ogiltig e-postadress",
diff --git a/supabase/migrations/20260709140000_add_customer_number_to_customers.sql b/supabase/migrations/20260709140000_add_customer_number_to_customers.sql
new file mode 100644
index 00000000..02f6036a
--- /dev/null
+++ b/supabase/migrations/20260709140000_add_customer_number_to_customers.sql
@@ -0,0 +1,16 @@
+-- Customer number (kundnummer) on customers, shown on the invoice (issue #914).
+--
+-- Nullable free text set by the user. Deliberately NO unique constraint in v1:
+-- existing rows, register imports, and provider syncs must not start failing
+-- on duplicates. Auto-numbering and uniqueness can be layered on later.
+--
+-- No RLS changes needed: customers already has company-scoped policies and a
+-- plain column add inherits them.
+
+ALTER TABLE public.customers
+ ADD COLUMN IF NOT EXISTS customer_number text;
+
+COMMENT ON COLUMN public.customers.customer_number IS
+ 'User-assigned customer number (kundnummer) printed on invoices. Free text, not unique in v1.';
+
+NOTIFY pgrst, 'reload schema';
diff --git a/tests/helpers.ts b/tests/helpers.ts
index 9035a118..cf2a6022 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -400,6 +400,7 @@ export function makeCustomer(overrides: Partial = {}): Customer {
company_id: 'company-1',
name: 'Test AB',
customer_type: 'swedish_business',
+ customer_number: null,
email: 'kontakt@test.se',
phone: null,
address_line1: 'Storgatan 1',
diff --git a/types/index.ts b/types/index.ts
index 3bc590f9..088229e0 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -557,6 +557,10 @@ export interface Customer {
name: string
customer_type: CustomerType
+ // User-assigned customer number (kundnummer) shown on invoices.
+ // Free text, no uniqueness enforced in v1.
+ customer_number: string | null
+
// Contact
email: string | null
phone: string | null
@@ -1129,6 +1133,7 @@ export interface TaxRate {
export interface CreateCustomerInput {
name: string
customer_type: CustomerType
+ customer_number?: string | null
email?: string
phone?: string
address_line1?: string