visma-eaccounting — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited visma-eaccounting (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
⚠️ COMMUNITY SKILL - NOT OFFICIAL VISMA DOCUMENTATION
>
This is a community-created skill based on public Visma eAccounting API documentation. Always verify code against official Visma documentation before production use.
>
Skill Version: 1.0.0 Compatible with: Visma eAccounting API v2 (as of February 2026) Status: Community-maintained License: MIT
This skill provides comprehensive guidance for integrating with the Visma eAccounting API (v2), covering authentication, invoice management, customer/supplier operations, receipt uploads, and accounting workflows.
Created for: Developers building integrations with Visma eAccounting Maintained by: Community contributors Contributions: Welcome! Please verify all code against official Visma API documentation
Base URLs:
https://eaccountingapi.vismaonline.com/v2https://eaccountingapi-sandbox.test.vismaonline.com/v2https://eaccountingapi.vismaonline.com/scalar/v2Key Capabilities:
Visma eAccounting uses OAuth 2.0 with OpenID Connect for authentication.
Step 1: Authorization Request
const authUrl = new URL('https://identity.vismaonline.com/connect/authorize');
authUrl.searchParams.append('client_id', YOUR_CLIENT_ID);
authUrl.searchParams.append('redirect_uri', YOUR_REDIRECT_URI);
authUrl.searchParams.append('scope', 'ea:api ea:sales ea:purchase ea:accounting offline_access');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('state', generateRandomState()); // CSRF protection
authUrl.searchParams.append('prompt', 'select_account');
// Redirect user to authUrl.toString()Available Scopes:
ea:api - Required base scopeea:sales - Sales/invoice operationsea:purchase - Purchase/supplier operationsea:accounting - Accounting operationsoffline_access - Refresh token (recommended)Step 2: Exchange Code for Token
const tokenResponse = await fetch('https://identity.vismaonline.com/connect/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: YOUR_REDIRECT_URI
})
});
const tokens = await tokenResponse.json();
// tokens.access_token - expires in 1 hour
// tokens.refresh_token - long-lived, store securelyStep 3: Refresh Access Token
const refreshResponse = await fetch('https://identity.vismaonline.com/connect/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken
})
});
const newTokens = await refreshResponse.json();
// Store new refresh_token (invalidates old one)Important Notes:
async function callVismaAPI(endpoint, method = 'GET', body = null) {
const url = `https://eaccountingapi.vismaonline.com/v2${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
};
if (body && method !== 'GET') {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.message || response.statusText}`);
}
return response.json();
}Visma API uses OData query parameters:
// Get customers with pagination
const customers = await callVismaAPI('/customers?$top=50&$skip=0');
// Filter examples
const activeCustomers = await callVismaAPI("/customers?$filter=IsActive eq true");
const recentInvoices = await callVismaAPI("/customerinvoices?$filter=InvoiceDate gt 2024-01-01");
// Sorting
const sorted = await callVismaAPI("/customers?$orderby=Name asc");async function createCustomer(customerData) {
const customer = {
Name: customerData.name,
CorporateIdentityNumber: customerData.orgNumber, // Optional
Email: customerData.email,
InvoiceAddress1: customerData.address,
InvoiceCity: customerData.city,
InvoicePostalCode: customerData.postalCode,
InvoiceCountryCode: customerData.countryCode || 'SE',
Phone: customerData.phone,
VatNumber: customerData.vatNumber, // EU VAT number if applicable
IsActive: true
};
return await callVismaAPI('/customers', 'POST', customer);
}async function getCustomer(customerId) {
return await callVismaAPI(`/customers/${customerId}`);
}
async function searchCustomers(searchTerm) {
// Search by name or number
return await callVismaAPI(
`/customers?$filter=contains(Name,'${searchTerm}') or contains(CustomerNumber,'${searchTerm}')`
);
}async function updateCustomer(customerId, updates) {
return await callVismaAPI(`/customers/${customerId}`, 'PUT', updates);
}Visma provides two endpoints for invoice creation:
async function createInvoiceDraft(invoiceData) {
const draft = {
CustomerId: invoiceData.customerId,
InvoiceDate: invoiceData.invoiceDate || new Date().toISOString().split('T')[0],
DueDate: invoiceData.dueDate,
DeliveryDate: invoiceData.deliveryDate,
YourReference: invoiceData.yourReference,
OurReference: invoiceData.ourReference,
InvoiceRows: invoiceData.rows.map(row => ({
ArticleId: row.articleId, // Optional - can use ArticleNumber instead
ArticleNumber: row.articleNumber,
Description: row.description,
Quantity: row.quantity,
UnitPrice: row.unitPrice,
VatPercent: row.vatPercent || 25, // Default Swedish VAT
DiscountPercent: row.discountPercent || 0
}))
};
return await callVismaAPI('/customerinvoicedrafts', 'POST', draft);
}async function convertDraftToInvoice(draftId, sendType = 'Manual') {
// sendType options: 'Manual', 'Email', 'EInvoice', 'AutoInvoice'
const invoice = {
CreatedFromDraftId: draftId,
SendType: sendType
};
return await callVismaAPI('/customerinvoices', 'POST', invoice);
}async function getInvoice(invoiceId) {
return await callVismaAPI(`/customerinvoices/${invoiceId}`);
}
async function getInvoicePDF(invoiceId) {
const pdfData = await callVismaAPI(`/customerinvoices/${invoiceId}/pdf`);
// pdfData.Url or pdfData.TemporaryUrl - download link
return pdfData.TemporaryUrl;
}async function sendInvoiceByEmail(invoiceId, emailAddress) {
return await callVismaAPI(
`/customerinvoices/${invoiceId}/email`,
'POST',
{ EmailAddress: emailAddress }
);
}async function creditInvoice(invoiceId, creditRows) {
const credit = {
InvoiceId: invoiceId,
CreditRows: creditRows.map(row => ({
RowId: row.originalRowId,
CreditedAmount: row.amount
}))
};
return await callVismaAPI('/customerinvoices/credit', 'POST', credit);
}async function voidInvoice(invoiceId) {
return await callVismaAPI(`/customerinvoices/${invoiceId}/void`, 'POST');
}Visma eAccounting supports attaching documents to various entities (invoices, vouchers, etc.).
async function uploadDocument(fileBuffer, fileName, mimeType) {
// Step 1: Upload file to storage
const formData = new FormData();
formData.append('file', fileBuffer, fileName);
const uploadResponse = await fetch(
'https://eaccountingapi.vismaonline.com/v2/files',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
},
body: formData
}
);
const uploadResult = await uploadResponse.json();
return uploadResult.FileId; // Use this to attach to entities
}
async function attachToInvoiceDraft(draftId, fileId, description) {
const attachment = {
CustomerInvoiceDraftId: draftId,
FileId: fileId,
Description: description || 'Receipt'
};
return await callVismaAPI('/salesdocumentattachments', 'POST', attachment);
}For Visma Scanner integration (receipts → supplier invoices):
async function createSupplierInvoiceFromReceipt(receiptData) {
// Upload receipt image/PDF
const fileId = await uploadDocument(
receiptData.fileBuffer,
receiptData.fileName,
receiptData.mimeType
);
// Create supplier invoice draft
const supplierInvoice = {
SupplierId: receiptData.supplierId,
InvoiceDate: receiptData.invoiceDate,
DueDate: receiptData.dueDate,
InvoiceNumber: receiptData.invoiceNumber,
TotalAmount: receiptData.totalAmount,
VatAmount: receiptData.vatAmount,
SupplierInvoiceRows: receiptData.rows.map(row => ({
AccountNumber: row.accountNumber,
Description: row.description,
Amount: row.amount,
VatPercent: row.vatPercent
}))
};
const invoice = await callVismaAPI('/supplierinvoices', 'POST', supplierInvoice);
// Attach receipt
await callVismaAPI('/purchasedocumentattachments', 'POST', {
SupplierInvoiceId: invoice.Id,
FileId: fileId,
Description: 'Receipt'
});
return invoice;
}For more advanced accounting, use ledger items directly:
async function createVoucherWithInvoice(voucherData) {
const voucher = {
TransactionDate: voucherData.date,
Description: voucherData.description,
CustomerLedgerItems: [{
CustomerId: voucherData.customerId,
InvoiceNumber: voucherData.invoiceNumber,
InvoiceDate: voucherData.invoiceDate,
DueDate: voucherData.dueDate,
Amount: voucherData.amount,
VatAmount: voucherData.vatAmount,
Rows: voucherData.rows.map(row => ({
AccountNumber: row.accountNumber,
Amount: row.amount,
VatCode: row.vatCode
}))
}]
};
return await callVismaAPI('/v2/vouchers', 'POST', voucher);
}async function updateOpeningBalances(balances) {
// Only works on first fiscal year
const payload = balances.map(balance => ({
AccountNumber: balance.accountNumber,
Balance: balance.balance
}));
return await callVismaAPI('/fiscalyears/openingbalances', 'PUT', payload);
}async function getFiscalYears() {
return await callVismaAPI('/fiscalyears');
}async function createArticle(articleData) {
const article = {
Number: articleData.number,
Name: articleData.name,
Description: articleData.description,
SalesPrice: articleData.salesPrice,
VatRate: articleData.vatRate || 25,
SalesAccount: articleData.salesAccount || 3000,
IsActive: true,
Unit: articleData.unit || 'pcs'
};
return await callVismaAPI('/articles', 'POST', article);
}async function createSupplier(supplierData) {
const supplier = {
Name: supplierData.name,
CorporateIdentityNumber: supplierData.orgNumber,
Email: supplierData.email,
Address: supplierData.address,
City: supplierData.city,
PostalCode: supplierData.postalCode,
CountryCode: supplierData.countryCode || 'SE',
Phone: supplierData.phone,
BankAccountNumber: supplierData.bankAccount,
IsActive: true
};
return await callVismaAPI('/suppliers', 'POST', supplier);
}async function robustAPICall(endpoint, method = 'GET', body = null, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch(
`https://eaccountingapi.vismaonline.com/v2${endpoint}`,
{
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
}
);
// Handle 401 - Token expired
if (response.status === 401) {
accessToken = await refreshAccessToken();
continue; // Retry with new token
}
// Handle 429 - Rate limit
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 5;
await sleep(retryAfter * 1000);
continue;
}
if (!response.ok) {
const error = await response.json();
throw new VismaAPIError(error.message, response.status, error);
}
return await response.json();
} catch (error) {
if (attempt === retries - 1) throw error;
await sleep(1000 * Math.pow(2, attempt)); // Exponential backoff
}
}
}
class VismaAPIError extends Error {
constructor(message, statusCode, details) {
super(message);
this.name = 'VismaAPIError';
this.statusCode = statusCode;
this.details = details;
}
}400 - Bad Request (validation error, check request body)401 - Unauthorized (token expired or invalid)403 - Forbidden (insufficient scope/permissions)404 - Not Found (resource doesn't exist)409 - Conflict (e.g., duplicate invoice number)429 - Too Many Requests (rate limit exceeded)500 - Internal Server Error (Visma issue, retry)class VismaTokenManager {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
}
async getValidToken() {
// Return cached token if still valid (with 5 min buffer)
if (this.accessToken && this.expiresAt > Date.now() + 300000) {
return this.accessToken;
}
// Refresh if we have a refresh token
if (this.refreshToken) {
await this.refresh();
return this.accessToken;
}
throw new Error('No valid token or refresh token available');
}
async refresh() {
const response = await fetch('https://identity.vismaonline.com/connect/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' +
Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken
})
});
const tokens = await response.json();
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token; // Store new refresh token!
this.expiresAt = Date.now() + (tokens.expires_in * 1000);
// Persist to secure storage
await this.saveTokens();
}
async saveTokens() {
// Implement secure storage (database, encrypted file, etc.)
}
}If user has multiple companies, they must select one during OAuth:
// In authorization URL, after successful login:
// User will see company dropdown if they have multiple companies
// To avoid this, ensure they've set a default company in settings
// To programmatically handle this, you may need to:
// 1. Get company list after initial auth
// 2. Store selected company ID
// 3. Use company-specific endpointsVisma eAccounting doesn't have native webhooks. For real-time sync:
$filter with timestamps: ModifiedUtc gt 2024-01-01T00:00:00ZAlways test with sandbox first:
https://eaccountingapi-sandbox.test.vismaonline.com/v2https://eaccounting-sandbox.test.vismaonline.comclass VismaInvoiceManager {
constructor(tokenManager) {
this.tokenManager = tokenManager;
this.baseUrl = 'https://eaccountingapi.vismaonline.com/v2';
}
async createAndSendInvoice(invoiceData) {
const token = await this.tokenManager.getValidToken();
try {
// 1. Create draft
console.log('Creating invoice draft...');
const draft = await this.apiCall('/customerinvoicedrafts', 'POST', {
CustomerId: invoiceData.customerId,
InvoiceDate: new Date().toISOString().split('T')[0],
DueDate: invoiceData.dueDate,
YourReference: invoiceData.reference,
InvoiceRows: invoiceData.items.map(item => ({
Description: item.description,
Quantity: item.quantity,
UnitPrice: item.price,
VatPercent: 25
}))
});
// 2. Upload attachments if any
if (invoiceData.attachments?.length > 0) {
console.log('Uploading attachments...');
for (const attachment of invoiceData.attachments) {
const fileId = await this.uploadFile(attachment);
await this.apiCall('/salesdocumentattachments', 'POST', {
CustomerInvoiceDraftId: draft.Id,
FileId: fileId,
Description: attachment.description
});
}
}
// 3. Convert to invoice and send
console.log('Converting to invoice and sending...');
const invoice = await this.apiCall('/customerinvoices', 'POST', {
CreatedFromDraftId: draft.Id,
SendType: 'Email'
});
console.log(`Invoice ${invoice.InvoiceNumber} created and sent!`);
return invoice;
} catch (error) {
console.error('Invoice creation failed:', error);
throw error;
}
}
async apiCall(endpoint, method, body) {
const token = await this.tokenManager.getValidToken();
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.message || response.statusText}`);
}
return response.json();
}
async uploadFile(attachment) {
const token = await this.tokenManager.getValidToken();
const formData = new FormData();
formData.append('file', attachment.buffer, attachment.filename);
const response = await fetch(`${this.baseUrl}/files`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const result = await response.json();
return result.FileId;
}
}The company must complete the startup guide in eAccounting before API access works. Solution: Log into eAccounting web interface, complete the setup wizard.
If "Choose default company" dropdown is missing during OAuth, the user has a default company set. Solution: In eAccounting, go to profile menu → "Choose default company" → Remove default (click star).
The API has rate limits. Implement exponential backoff and respect Retry-After headers.
PDF URL format changed in late 2024. Always use the latest URL from the API response, don't cache URL patterns.
When implementing Visma eAccounting integration:
| Operation | Method | Endpoint |
|---|---|---|
| List customers | GET | /customers |
| Create customer | POST | /customers |
| Get customer | GET | /customers/{id} |
| List invoices | GET | /customerinvoices |
| Create invoice draft | POST | /customerinvoicedrafts |
| Convert draft | POST | /customerinvoices |
| Get invoice PDF | GET | /customerinvoices/{id}/pdf |
| Send invoice email | POST | /customerinvoices/{id}/email |
| Credit invoice | POST | /customerinvoices/credit |
| Void invoice | POST | /customerinvoices/{id}/void |
| Upload file | POST | /files |
| Attach to invoice | POST | /salesdocumentattachments |
| List suppliers | GET | /suppliers |
| Create supplier invoice | POST | /supplierinvoices |
| Attach receipt | POST | /purchasedocumentattachments |
| List articles | GET | /articles |
| Create article | POST | /articles |
| Get fiscal years | GET | /fiscalyears |
| Update opening balance | PUT | /fiscalyears/openingbalances |
This skill provides the foundation for building robust Visma eAccounting integrations. Remember to always test in the sandbox environment and handle errors gracefully!
This skill is created by the community and is not officially endorsed by Visma. Always refer to the official Visma API documentation for the most accurate and up-to-date information.
This skill is provided "as-is" without any warranties, express or implied. The creators and contributors are not responsible for any issues, data loss, or problems arising from the use of code examples provided in this skill.
The Visma eAccounting API may change over time. This skill was last updated in February 2026 for API v2. Always verify:
Before deploying to production:
For official Visma API support, contact: [email protected]
For questions about this skill:
MIT License
Copyright (c) 2026 Community Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.Skill Version: 1.0.0 Last Updated: February 7, 2026 API Compatibility: Visma eAccounting API v2 Language: JavaScript/Node.js Status: Community-maintained Contributions: Welcome!
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.