Menu
Cookbooks
API reference
- Overview
- Companies
- Customers
- Invoices
- Articles
- Suppliers
- Supplier invoices
- Supplier payment files
- Expense claims
- Transactions
- Reconciliation
- Bank accounts
- Journal entries
- Voucher gap explanations
- Fiscal periods
- Accounts
- Fixed assets
- Documents
- Inbox items
- Dimensions
- Employees
- Salary runs
- Reports
- Imports
- Compliance check
- Skatteverket
- Peppol
- Webhooks
- Operations
- Health
Customers
CRM-side: who you invoice. Business and individual (sole-trader) customers with VIES validation.
Endpoints
GET/api/v1/companies/:companyId/customers: List customers for a company.GET/api/v1/companies/:companyId/customers/:id: Retrieve a single customer by id.POST/api/v1/companies/:companyId/customers: Create a customer.POST/api/v1/companies/:companyId/customers/bulk-create: Create up to 50 customers in one call (partial-success).PATCH/api/v1/companies/:companyId/customers/:id: Partially update a customer.DELETE/api/v1/companies/:companyId/customers/:id: Archive a customer (soft-delete).
GET /api/v1/companies/:companyId/customers
customers.list · scope customers:read
List customers for a company.
Returns active customers in created-first order. Pass ?include_archived=true to include archived rows. Use ?search to match against name or org_number.
Use when: You need a customer roster: for building a UI picker, syncing a CRM, or resolving a customer_id before creating an invoice.
Don't use for: Fetching a single customer you already know the id of: use GET /api/v1/companies/{companyId}/customers/{id}. Suppliers are a separate resource.
Pitfalls
- Archived customers are hidden by default; the dashboard makes the same choice.
- org_number is included so callers can match against external CRM identifiers, except where it is a natural person's identity number: a sole trader (enskild firma) has no org number of its own, so its org_number and vat_number come back null in the list. Read the record with GET /customers/{id} for the full value.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
customer_type | "individual" | "swedish_business" | "eu_business" | "non_eu_business" | no | Only customers of this type. |
search | string | no | Case-insensitive match on the name (anywhere) or the org number (prefix), 1-200 characters. |
include_archived | "true" | "false" | no | true also returns archived customers. Default: false. |
cursor | string | no | Opaque cursor from the previous page's meta.next_cursor. Omit for the first page. |
limit | number | no | Page size, 1-100 (default 50). Larger values are clamped to 100. |
Response fields
| Name | Type |
|---|---|
[].id | string |
[].name | string |
[].customer_type | "individual" | "swedish_business" | "eu_business" | "non_eu_business" |
[].email | string | null |
[].org_number | string | null |
[].vat_number | string | null |
[].default_payment_terms | number |
[].party_id | string | null (optional) |
[].archived_at | string | null |
[].created_at | string |
Example response
{
"data": [
{
"id": "a8f1…",
"name": "Acme AB",
"customer_type": "swedish_business",
"email": "finance@acme.example",
"org_number": "556677-8899",
"vat_number": "SE556677889901",
"default_payment_terms": 30,
"archived_at": null,
"created_at": "2025-04-12T08:30:00Z"
}
],
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
}
}
GET /api/v1/companies/:companyId/customers/:id
customers.get · scope customers:read
Retrieve a single customer by id.
Returns the full customer record. Pass ?expand=invoices to embed any open invoices (sent / partially_paid / overdue) for the customer in the same response. Pass ?expand=party to embed the party (motpart) behind the customer: legal name, org and VAT number, country, the SCB company-register summary and what the ledger has seen for it. Private individuals have no party.
Use when: You need the full customer record: address, payment terms, VAT validation status, contact details: before invoicing or syncing to another system.
Don't use for: Listing customers (use the list endpoint). Looking up arbitrary supplier or employee records (different resources).
Pitfalls
- archived_at is non-null when the customer has been soft-deleted; the customer is still queryable by id but excluded from default lists.
- vat_number_validated reflects the last successful VIES check; it can become stale if the EU registry revokes a number.
- personal_number is always returned in the masked form ********-1234; the stored value is encrypted and never leaves the API.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
expand | string | no | Comma-separated related records to embed: invoices, party. An unknown key returns 400 VALIDATION_ERROR. |
Response fields
| Name | Type |
|---|---|
id | string |
name | string |
customer_type | string |
customer_number | string | null |
contact_person | string | null |
email | string | null |
phone | string | null |
invoice_email_cc_addresses | string[] | null |
invoice_email_bcc_addresses | string[] | null |
address_line1 | string | null |
address_line2 | string | null |
postal_code | string | null |
city | string | null |
country | string |
org_number | string | null |
vat_number | string | null |
vat_number_validated | boolean |
personal_number | string | null |
default_payment_terms | number |
notes | string | null |
party_id | string | null |
party | object | null (optional) |
archived_at | string | null |
created_at | string |
updated_at | string |
Example response
{
"data": {
"id": "a8f1…",
"name": "Acme AB",
"customer_type": "business",
"email": "finance@acme.example",
"org_number": "556677-8899",
"vat_number": "SE556677889901",
"vat_number_validated": true,
"country": "SE",
"default_payment_terms": 30,
"archived_at": null,
"created_at": "2025-04-12T08:30:00Z",
"updated_at": "2026-04-30T11:22:09Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/customers
customers.create · scope customers:write
Create a customer.
Creates a new customer for the company. Requires Idempotency-Key (UUID). Supports ?dry_run=true for input validation without committing: the dry-run response shows the would-be record minus id and timestamps. EU-business customers with a VAT number are auto-validated against VIES on commit.
Use when: You need to register a new customer before invoicing them. Use dry-run first to catch validation errors before committing.
Don't use for: Updating an existing customer (PATCH instead). Creating suppliers (different resource).
Pitfalls
- Idempotency-Key is mandatory: calls without it return 400 VALIDATION_ERROR.
- org_number uniqueness is enforced at the database level; duplicate inserts return 409 CUSTOMER_DUPLICATE_ORG_NUMBER.
- A personnummer-shaped org_number on customer_type=individual is treated as the personnummer submitted in the wrong field: it is stored encrypted as personal_number, returned masked (********-1234), and org_number is left empty. Prefer passing it as personal_number. Next to a different personal_number in the same body it is a 400.
- An org_number shaped like a Swedish personnummer is accepted on customer_type=swedish_business: a sole trader (enskild firma) has no separate org number, so its owner's personnummer is the firm's identifier, and the list endpoint masks it. It is rejected for eu_business and non_eu_business, which cannot have one.
- personal_number is accepted only for customer_type=individual, stored encrypted, and returned in the masked form ********-1234.
- If default_payment_terms is omitted, it defaults to the company setting invoice_default_days, falling back to 30.
- VIES validation runs only on commit. Dry-run skips the external call and leaves vat_number_validated=false in the preview.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required |
|---|---|---|
name | string | yes |
customer_type | "individual" | "swedish_business" | "eu_business" | "non_eu_business" | yes |
customer_number | string | null | no |
contact_person | string | null | no |
email | string | no |
phone | string | no |
invoice_email_cc_addresses | string[] | null | no |
invoice_email_bcc_addresses | string[] | null | no |
address_line1 | string | no |
address_line2 | string | no |
postal_code | string | no |
city | string | no |
country | string | no |
org_number | string | no |
vat_number | string | no |
personal_number | string | null | no |
language | "sv" | "en" | no |
default_payment_terms | number | no |
notes | string | no |
Response fields
| Name | Type |
|---|---|
id | string | null |
name | string |
customer_type | "individual" | "swedish_business" | "eu_business" | "non_eu_business" |
customer_number | string | null |
contact_person | string | null |
email | string | null |
phone | string | null |
invoice_email_cc_addresses | string[] | null |
invoice_email_bcc_addresses | string[] | null |
address_line1 | string | null |
address_line2 | string | null |
postal_code | string | null |
city | string | null |
country | string |
org_number | string | null |
vat_number | string | null |
vat_number_validated | boolean |
personal_number | string | null |
default_payment_terms | number |
notes | string | null |
archived_at | string | null |
created_at | string | null |
updated_at | string | null |
Example request
{
"name": "Acme AB",
"customer_type": "swedish_business",
"email": "finance@acme.test",
"org_number": "556677-8899",
"default_payment_terms": 30
}
Example response
{
"data": {
"id": "0e9c…",
"name": "Acme AB",
"customer_type": "swedish_business",
"email": "finance@acme.test",
"org_number": "556677-8899",
"vat_number_validated": false,
"default_payment_terms": 30,
"archived_at": null,
"created_at": "2026-05-12T16:00:00Z",
"updated_at": "2026-05-12T16:00:00Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/customers/bulk-create
customers.bulk-create · scope customers:write
Create up to 50 customers in one call (partial-success).
Bulk-create endpoint mirroring /invoices/bulk-create. Each customer is validated and inserted independently: per-item failures do not roll back items that succeeded. Returns a results array plus a summary. Idempotent over the whole batch. Dry-runnable.
Use when: You're importing a roster of customers from another CRM, or seeding a fresh company with its existing client list. Use dry-run first to validate the batch.
Don't use for: Updating existing customers: PATCH /customers/{id} once per customer. Bulk uploads of > 50 customers: split into pages of 50. Transactional all-or-nothing imports: passing all_or_nothing: true returns 501 NOT_IMPLEMENTED.
Pitfalls
- Idempotency-Key is mandatory and covers the WHOLE batch. A retried bulk-create returns the cached full response: it does not retry only the failed items.
- Passing all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist; omit the flag or pass false.
- org_number uniqueness is enforced at the DB level: items with duplicates fail individually with CUSTOMER_DUPLICATE_ORG_NUMBER.
- VIES validation for eu_business customers is best-effort per item; a VIES timeout leaves vat_number_validated=false but does NOT fail the item.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required |
|---|---|---|
customers | object[] | yes |
all_or_nothing | boolean | no |
Response fields
| Name | Type |
|---|---|
results | object[] |
summary | object |
Example request
{
"customers": [
{
"name": "Acme AB",
"customer_type": "swedish_business",
"org_number": "556677-8899"
},
{
"name": "Foo OY",
"customer_type": "eu_business",
"vat_number": "FI12345678"
}
]
}
Example response
{
"data": {
"results": [
{
"ok": true,
"request_index": 0,
"data": {
"id": "0e9c…",
"name": "Acme AB"
}
},
{
"ok": true,
"request_index": 1,
"data": {
"id": "4d2a…",
"name": "Foo OY"
}
}
],
"summary": {
"total": 2,
"succeeded": 2,
"failed": 0
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PATCH /api/v1/companies/:companyId/customers/:id
customers.update · scope customers:write
Partially update a customer.
Patches the customer with the supplied fields. All fields optional. Idempotent (mandatory Idempotency-Key). Dry-runnable. When vat_number changes on an eu_business customer, VIES re-validation runs on commit (best-effort).
Use when: You need to change a customer's contact details, payment terms, address, or VAT registration. Use dry-run first to confirm the merged record before committing.
Don't use for: Archiving a customer (use DELETE: sets archived_at). Replacing the entire record (no PUT verb is exposed; PATCH is partial).
Pitfalls
- Idempotency-Key is mandatory; calls without it return 400.
- org_number uniqueness is enforced at DB level: 23505 → 409 CUSTOMER_DUPLICATE_ORG_NUMBER.
- VIES re-validation is best-effort and runs only on commit. A VIES timeout does not fail the update.
- personal_number: a plaintext value is stored encrypted (individual customers only); the masked form a read returned (********-1234) means "leave unchanged" and is never stored; null clears it. Changing customer_type away from individual clears any stored personal_number.
- An org_number shaped like a Swedish personnummer is accepted on customer_type=swedish_business: an enskild firma has no separate org number, so it is the firm's identifier, and the list endpoint masks it. It is rejected for eu_business and non_eu_business (400 CUSTOMER_ORG_NUMBER_IS_PERSONAL). On an individual it is the personnummer in the wrong field: it is stored encrypted as personal_number and org_number is cleared; next to a different personal_number in the same body it is 400 CUSTOMER_PERSONAL_NUMBER_CONFLICT.
Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required |
|---|---|---|
name | string | no |
customer_type | "individual" | "swedish_business" | "eu_business" | "non_eu_business" | no |
customer_number | string | null | no |
contact_person | string | null | no |
email | string | no |
phone | string | no |
invoice_email_cc_addresses | string[] | null | no |
invoice_email_bcc_addresses | string[] | null | no |
address_line1 | string | no |
address_line2 | string | no |
postal_code | string | no |
city | string | no |
country | string | no |
org_number | string | no |
vat_number | string | no |
personal_number | string | null | no |
language | "sv" | "en" | no |
default_payment_terms | number | no |
notes | string | no |
Response fields
| Name | Type |
|---|---|
id | string |
name | string |
customer_type | string |
customer_number | string | null |
contact_person | string | null |
email | string | null |
phone | string | null |
invoice_email_cc_addresses | string[] | null |
invoice_email_bcc_addresses | string[] | null |
address_line1 | string | null |
address_line2 | string | null |
postal_code | string | null |
city | string | null |
country | string |
org_number | string | null |
vat_number | string | null |
vat_number_validated | boolean |
personal_number | string | null |
default_payment_terms | number |
notes | string | null |
party_id | string | null |
party | object | null (optional) |
archived_at | string | null |
created_at | string |
updated_at | string |
Example request
{
"default_payment_terms": 14,
"notes": "New payment terms agreed 2026-05-12."
}
Example response
{
"data": {
"id": "0e9c…",
"name": "Acme AB",
"default_payment_terms": 14,
"notes": "New payment terms agreed 2026-05-12."
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
DELETE /api/v1/companies/:companyId/customers/:id
customers.delete · scope customers:write
Archive a customer (soft-delete).
Sets archived_at on the customer; the record is preserved (invoices and audit history remain intact) but excluded from default list responses. To un-archive, PATCH archived_at back to null. Idempotent: archiving an already-archived customer is a no-op. Dry-runnable.
Use when: You want to remove a customer from active rosters without losing their history. Idempotent: re-archiving is safe.
Don't use for: Permanently deleting a customer with all history: the public API does not expose hard-delete. GDPR erasure requests go through a dedicated workflow.
Pitfalls
- Idempotency-Key is mandatory.
- A customer with any open invoice (sent / partially_paid / overdue) cannot be archived: returns 409 CUSTOMER_HAS_INVOICES. Issue a kreditfaktura first if you need to close the relationship cleanly. This protects ML 17 kap 24§: the customer record is the canonical source of buyer name/address for invoice reissuance.
- 204 No Content is returned on success: there is no response body to parse.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Example response
{
"data": null,
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}