A production-tested Java library for filing Norwegian VAT returns (MVA-melding) via ID-porten and Altinn.
Built on real accounting platform code. Handles the full 15-step flow — period generation, tax line aggregation, XML building, ID-porten PKCE login, Altinn submission, signing, and feedback polling.
- Generates the correct filing calendar (monthly / bi-monthly / yearly) with Norwegian public-holiday-adjusted deadlines
- Aggregates ledger entries into the
mvaSpesifikasjonslinjelines the XML requires, applying all Norwegian sign rules (negation, zero-VAT, reverse-charge double-entry for codes 81/83/86/88/91) - Builds and validates the
mvaMeldingXML against Skatteetaten's schema - Handles the complete Altinn 3 submission flow (create instance → sign → complete → poll feedback)
- Parses the
betalingsinformasjon.xmlfeedback to extract payment amount, KID, IBAN, and deadline - Ports-and-adapters architecture — bring your own ledger data
<dependency>
<groupId>no.skatt</groupId>
<artifactId>mva-melding-java</artifactId>
<version>1.0.0</version>
</dependency>Copy application.yml to your project and fill in the values marked CHANGE_ME.
Key settings:
mva:
idporten:
client-id: your-digdir-client-id
kid: your-key-id
private-key: your-pkcs8-private-key-base64
redirect-uri: https://your-app.example.com/api/mva/callbackThis is the only interface you must implement. It connects the library to your accounting database:
@Component
public class MyLedgerAdapter implements LedgerPort {
@Autowired private JournalRepository repo;
@Override
public List<LedgerEntry> findVatEntries(VatPeriod period, VatCodeRules rules) {
return repo.findByDateRange(
period.getDocStartDate(),
period.getDocEndDate(),
rules.getAllVatReturnCodes(),
period.getOrganisationNumber()
).stream().map(this::toLedgerEntry).toList();
}
@Override
public CompanyInfo getCompanyInfo(String orgNo) {
Company c = repo.findByOrgNo(orgNo);
return new CompanyInfo(c.getOrgNo(), c.getName(), c.getFirstName(), c.getLastName());
}
@Override
public Map<String, String> getVatCodeNames(String orgNo) {
return repo.findVatCodes(orgNo).stream()
.collect(Collectors.toMap(VatCode::getBid, VatCode::getName));
}
}Sign convention: return amounts with their natural accounting sign — output VAT (credit accounts 2700–2708) as positive, input VAT (debit accounts 2710–2720) as negative. The library applies the Norwegian MVA sign rules on top.
@Autowired MvaFilingService filing;
// Initialise periods for 2024
List<VatPeriod> periods = filing.initialisePeriods(
orgNo, 2024, ReportingFrequency.TWO_MONTHLY, VatReportType.GENERAL, holidays);
myRepo.saveAll(periods);
// Validate a period
VatPeriod updated = filing.validate(period, null, null);
myRepo.save(updated);
// Get login URL → redirect user to it
String loginUrl = filing.getLoginUrl(period, userId, "https://my-app.example.com/api/mva/callback");
// In your /callback controller:
filing.handleLoginCallback(request.getParam("code"), request.getParam("state"));
// Submit
updated = filing.submitAndSign(period, userId);
myRepo.save(updated);
// Poll for feedback (call every 5 minutes from a scheduler)
if (period.getFilingState() == FilingState.UPLOAD_COMPLETE) {
updated = filing.pollFeedback(period, userId);
myRepo.save(updated);
}INIT → validate() → VALIDATED
→ submitAndSign() → SUBMITTED → SIGNED → DATA_COMPLETE → UPLOAD_COMPLETE
→ pollFeedback() → FEEDBACK_RECEIVED
Each step returns an updated VatPeriod. Persist it after every step — the library is stateless.
- Go to selvbetjening.digdir.no
- Create a new integration: type = "API-klient"
- Scopes:
openid skatteetaten:mvameldinginnsending skatteetaten:mvameldingvalidering - Token endpoint auth:
private_key_jwt - Generate an RSA key pair, upload the public key JWKS, note the key ID
- Put the private key (PKCS8, base64) in
mva.idporten.private-key
Generate a key pair:
openssl genrsa -out rsa_key.pem 2048
openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out rsa_key_pkcs8.pem
# Upload the public part to Digdir:
openssl rsa -in rsa_key.pem -pubout -out rsa_key_pub.pemThe mva.codes.* configuration encodes how each Norwegian MVA-kode is treated when building the return. The defaults match the 2024 Norwegian standard chart of accounts — you should not need to change them unless Skatteetaten updates the code set.
Key rule groups:
- baseNegateCodes / vatNegateCodes: amounts are sign-flipped for these codes (output VAT on sales is a liability, so the sign is inverted in the XML)
- doubleVatCodes: reverse-charge codes (81, 83, 86, 88, 91) that generate both an output and an input line
- zeroVatCodes: reported with
sats="0"regardless of the ledger rate - nullBasicCodes: basis amount suppressed in the XML
# Start Redis
docker run -p 6379:6379 redis:7-alpine
# Run tests
mvn test
# Start the app
cp src/main/resources/application.yml src/main/resources/application-local.yml
# Edit application-local.yml and fill in your values
mvn spring-boot:run -Dspring-boot.run.profiles=localApache 2.0