Initial setup

The client library is available as a Nuget package. The client library (and associated Nuget package) is updated regularly as new functionality is added.

To install the Nuget package, follow these steps in Visual Studio/Rider:

  1. Select TOOLS -> Nuget Package Manager -> Manage Nuget Packages Solution…

  2. Search for Digipost.Api.Client. Multiple packages will appear. Install those necessary for you. Make sure you DON’T install the digipost-api-client packages. Those are .NET Framework libraries with an unfortunately similar name. If you’re looking for the .NET Framework documention, please see version 8.3. If you would like pre-releases of this package, make sure Include Prerelease` is enabled. Please refer to documentation for your version of Visual Studio for detailed instructions.
  3. Select which Digipost.Api.Client.X libraries you need and click Install for each.

Install and use enterprise certificate

SSL Certificates are small data files that digitally bind a cryptographic key to an organization's details. When installed on a web server, it activates the padlock and the https protocol (over port 443) and allows secure connections from a web server to a browser.

To communicate over HTTPS you need to sign your request with a enterprise certificate. The enterprise certificate can be loaded directly from file or from the Windows certificate store.

The following steps will install the certificate in the your certificate store. This should be done on the server where your application will run. For more information, please see the Microsoft Documentation.

The path and password to the certificate must be put somewhere safe.

The path on Windows is:

%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json

On MacOS/Linux the path is:

~/.microsoft/usersecrets/<user_secrets_id>/secrets.json

Add the following UserSecretsId element to your .csproj file:

<PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <UserSecretsId>enterprise-certificate</UserSecretsId>
</PropertyGroup>

This means that the element <user_secrets_id> in the path will be enterprise-certificate.

From the command line, navigate to the directory where the current .csproj file is located and run the following commands with your own certificate values:

dotnet user-secrets set "Certificate:Path:Absolute" "<your-certificate.p12>"
dotnet user-secrets set "Certificate:Password" "<your-certificate-password>"
Trust the Certificate on Windows:
  1. Double-click on the actual certificate file (CertificateName.p12)
  2. Save the sertificate in Current User or Local Machine and click Next. If you are running the client library from a system account, but debugging from a different user, please install it on Local Machine, as this enables loading it from any user.
  3. Use the suggested filename. Click Next
  4. Enter password for private key and select Mark this key as exportable … Click Next
  5. Choose Automatically select the certificate store based on the type of certificate
  6. Click Next and Finish
  7. Accept the certificate if prompted.
  8. When prompted that the import was successful, click Ok
Trust the Certificate on MacOS:
  1. Open Keychain Access
  2. Choose login keychain
  3. Press File and then Import
  4. Choose the enterprise certificate and add it
Trust the Certificate on Linux:

Download the root and intermediate certificates from Difi for your enterprise certificate provider. Note the renaming to have .crt ending for update-ca-certificates:

sudo cp Buypass_Class_3_Test4_Root_CA.pem /usr/local/share/ca-certificates/Buypass_Class_3_Test4_Root_CA.crt
sudo cp Buypass_Class_3_Test4_CA_3.pem /usr/local/share/ca-certificates/Buypass_Class_3_Test4_CA_3.crt
sudo update-ca-certificates

Create ClientConfig and DigipostClient:

// The actual sender of the message. The broker is the owner of the organization certificate 
// used in the library. The broker id can be retrieved from your Digipost organization account.
const int brokerId = 12345;
            
var broker = new Broker(brokerId);
var clientConfig = new ClientConfig(broker, Environment.Production);

var client = new DigipostClient(clientConfig, CertificateReader.ReadCertificate());

Where CertificateReader.ReadCertificate() is:

var pathToSecrets = $"{System.Environment.GetEnvironmentVariable("HOME")}/.microsoft/usersecrets/smoke-certificate/secrets.json";
_logger.LogDebug($"Reading certificate details from secrets file: {pathToSecrets}");
var fileExists = File.Exists(pathToSecrets);

if (!fileExists)
{
    _logger.LogDebug($"Did not find file at {pathToSecrets}");
}
            
var certificateConfig = File.ReadAllText(pathToSecrets);
var deserializeObject = JsonConvert.DeserializeObject<Dictionary<string, string>>(certificateConfig);

deserializeObject.TryGetValue("Certificate:Path:Absolute", out var certificatePath);
deserializeObject.TryGetValue("Certificate:Password", out var certificatePassword);

_logger.LogDebug("Reading certificate from path found in secrets file: " + certificatePath);

return new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.Exportable);

Load enterprise certificate from file

If there is a reason for not loading the certificate from the Windows certificate store, you can use the constructor taking in a X509Certificate2:

var clientConfig = new ClientConfig(broker, Environment.Production);
var enterpriseCertificate =
    new X509Certificate2(
        @"C:\Path\To\Certificate\Cert.p12",
        "secretPasswordProperlyInstalledAndLoaded",
        X509KeyStorageFlags.Exportable
    );

var client = new DigipostClient(clientConfig, enterpriseCertificate);

[Warning: This is technically supported on other operating systems than Windows, but we’ve not tested that thoroughly. Use at your own risk.] Load enterprise certificate from thumbprint in code

  1. Start mmc.exe (Click windowsbutton and type mmc.exe)
  2. Choose File -> Add/Remove Snap-in… (Ctrl + M)
  3. Mark certificate and click Add >
  4. If the certificate was installed in Current User choose My User Account and if installed on Local Machine choose Computer Account. Then click Finish and then OK
  5. Expand Certificates node, select Personal and open Certificates
  6. Double-click on the installed certificate
  7. Go to the Details tab
  8. Scroll down to Thumbprint
  9. Copy the thumbprint and load as shown below
 var clientConfig = new ClientConfig(broker, Environment.Production);
 var client = new DigipostClient(clientConfig, thumbprint: "84e492a972b7e...");

Load root element of Digipost rest api

The Digipost API is a HATEOAS-driven REST API. If you query the root you will get a response containing the Digipost public key and a list of hyperlinks to resources you can use with your sender or as a broker for a sender. This information is used behind the scenes by the client library to get the correct urls for different resources.

Fetch the default root. This root is normally what you want to use if you are not a broker for another sender.

var root = client.GetRoot(new ApiRootUri());

Fetch the root for a sender.

var root = client.GetRoot(new ApiRootUri(12345));

You can use this resource to safely test your integration or to fetch Digipost public key.

Proxy support

We have basic proxy support in the client. To use a proxy you need to define it in ClientConfig with a NetworkCredential for authentication.

var clientConfig = new ClientConfig(broker, Environment.Production)
{
    WebProxy = new WebProxy("10.0.0.1"),
    Credential = new NetworkCredential("foo", "bar")
};
 var client = new DigipostClient(clientConfig, thumbprint: "84e492a972b7e...");

Send messages

Client configuration

ClientConfig is a container for all the connection specific parameters that you can set.

// The actual sender of the message. The broker is the owner of the organization certificate
// used in the library. The broker id can be retrieved from your Digipost organization account.
var broker = new Broker(12345);

// The sender is what the receiver of the message sees as the sender of the message.
// Sender and broker id will both be your organization's id if you are sending on behalf of yourself.
var sender = new Sender(67890);

var clientConfig = new ClientConfig(broker, Environment.Production);

var client = new DigipostClient(clientConfig, CertificateReader.ReadCertificate());

Send one letter to recipient via personal identification number

var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt")
);

var result = client.SendMessage(message);

Other recipient types

There are other recipient types available to identify recipients of messages. Note that some recipient types may require special permissions to be set up in order to be used.

var recipient = new RecipientByNameAndAddress(
    fullName: "Ola Nordmann",
    addressLine1: "Prinsensveien 123",
    postalCode: "0460",
    city: "Oslo"
);

var primaryDocument = new Document(subject: "document subject", fileType: "pdf", path: @"c:\...\document.pdf");

var message = new Message(sender, recipient, primaryDocument);
var result = client.SendMessage(message);

Send one letter with multiple attachments

var primaryDocument = new Document(subject: "Primary document", fileType: "pdf", path: @"c:\...\document.pdf");
var attachment1 = new Document(subject: "Attachment 1", fileType: "txt", path: @"c:\...\attachment_01.txt");
var attachment2 = new Document(subject: "Attachment 2", fileType: "pdf", path: @"c:\...\attachment_02.pdf");

var message = new Message(
        sender,
        new RecipientById(IdentificationType.PersonalIdentificationNumber, id: "241084xxxxx"),
        primaryDocument
    ){Attachments = {attachment1, attachment2}};

var result = client.SendMessage(message);

Send letter with SMS notification

var primaryDocument = new Document(subject: "Primary document", fileType: "pdf", path: @"c:\...\document.pdf");

primaryDocument.SmsNotification = new SmsNotification(afterHours: 0); //SMS reminder after 0 hours
primaryDocument.SmsNotification.NotifyAtTimes.Add(new DateTime(2015, 05, 05, 12, 00, 00)); //new reminder at a specific date

var message = new Message(
    sender,
    new RecipientById(identificationType: IdentificationType.PersonalIdentificationNumber, id: "311084xxxx"),
    primaryDocument
);

var result = client.SendMessage(message);

Send letter with fallback to print if the user does not exist in Digipost

In cases where the recipient is not a Digipost user, it is also possible to use the recipient’s name and address for physical mail delivery.

var recipient = new RecipientByNameAndAddress(
    fullName: "Ola Nordmann",
    addressLine1: "Prinsensveien 123",
    postalCode: "0460",
    city: "Oslo"
);

var printDetails =
    new PrintDetails(
        printRecipient: new PrintRecipient(
            "Ola Nordmann",
            new NorwegianAddress("0460", "Oslo", "Prinsensveien 123")),
        printReturnRecipient: new PrintReturnRecipient(
            "Kari Nordmann",
            new NorwegianAddress("0400", "Oslo", "Akers Àle 2"))
    );

var primaryDocument = new Document(subject: "document subject", fileType: "pdf", path: @"c:\...\document.pdf");

var messageWithFallbackToPrint = new Message(sender, recipient, primaryDocument)
{
    PrintDetails = printDetails
};

var result = client.SendMessage(messageWithFallbackToPrint);

Send message with request for registration

It is possible to send a message to a person who does not have a Digipost account, where the message triggers an SMS notification with a request for registration. The SMS notification says that if they register for a Digipost account the document will be delivered digitally.

The actual content of the SMS is not part of the request, it is stored as part of the Digipost sender account and must be agreed upon with Digipost, as well as the SMS sender ID or phone number.

If the user does not register for a Digipost account within the defined deadline, the document will be either delivered as physical mail or not at all. Be aware that using PersonalIdentificationNumber is required as Recipient to be able to deliver the document to the correct person.

The phone number provided SHOULD include the country code (i.e. +47). If the phone number does not start with either "+", "00" or "011", we will prepend "+47" if and only if the phone number string is 8 characters long. If this is not the case, the request is rejected.

In the following the document will be delivered as physical mail by Digipost if the recipient has not registered for a Digipost account by the defined deadline:

var recipient = new RecipientById(identificationType: IdentificationType.PersonalIdentificationNumber, id: "311084xxxx");
var documentGuid = Guid.NewGuid();

var requestForRegistration = new RequestForRegistration(
    DateTime.Now.AddDays(3),
    "+4711223344",
    null,
    new PrintDetails(
        printRecipient: new PrintRecipient(
            "Ola Nordmann",
            new NorwegianAddress("0460", "Oslo", "Prinsensveien 123")),
        printReturnRecipient: new PrintReturnRecipient(
            "Kari Nordmann",
            new NorwegianAddress("0400", "Oslo", "Akers Àle 2"))
    )
);

var primaryDocument = new Document(subject: "document subject", fileType: "pdf", path: @"c:\...\document.pdf")
{
    Guid = documentGuid.ToString()
};

var messageWithRequestForRegistration = new Message(sender, recipient, primaryDocument)
{
    RequestForRegistration = requestForRegistration
};

var result = client.SendMessage(messageWithRequestForRegistration);

If the sender wishes to send the document as physical mail through its own service, print details must not be included but be null instead.

var recipient = new RecipientById(identificationType: IdentificationType.PersonalIdentificationNumber, id: "311084xxxx");
var documentGuid = Guid.NewGuid();

var requestForRegistration = new RequestForRegistration(
    DateTime.Now.AddDays(3),
    "+4711223344",
    null,
    null
);

In this case the status of the delivery can be checked with the following:

var documentStatus = _digipostClient.GetDocumentStatus(sender).GetDocumentStatus(documentGuid).Result;

If documentDeliveryStatus is still “NOT_DELIVERED” after the expiry date, you know that the user did not register a Digipost account and the document is not delivered.

The documentGuid is the same as the one used when the originating message was sent.

Send letter with fallback to print if the user does not read the message within a certain deadline

var recipient = new RecipientByNameAndAddress(
    fullName: "Ola Nordmann",
    addressLine1: "Prinsensveien 123",
    postalCode: "0460",
    city: "Oslo"
);

var printDetails =
    new PrintDetails(
        printRecipient: new PrintRecipient(
            "Ola Nordmann",
            new NorwegianAddress("0460", "Oslo", "Prinsensveien 123")),
        printReturnRecipient: new PrintReturnRecipient(
            "Kari Nordmann",
            new NorwegianAddress("0400", "Oslo", "Akers Àle 2"))
    );

var primaryDocument = new Document(subject: "document subject", fileType: "pdf", path: @"c:\...\document.pdf");

var messageWithPrintIfUnread = new Message(sender, recipient, primaryDocument)
{
    PrintDetails = printDetails,
    DeliveryTime = DateTime.Now.AddDays(3),
    PrintIfUnread = new PrintIfUnread(DateTime.Now.AddDays(6), printDetails)
};

var result = client.SendMessage(messageWithPrintIfUnread);

Send letter to print without any Digipost addressing information (directly to print)


var printDetails =
    new PrintDetails(
        printRecipient: new PrintRecipient(
            "Ola Nordmann",
            new NorwegianAddress("0460", "Oslo", "Prinsensveien 123")),
        printReturnRecipient: new PrintReturnRecipient(
            "Kari Nordmann",
            new NorwegianAddress("0400", "Oslo", "Akers Àle 2"))
    );

var primaryDocument = new Document(subject: "document subject", fileType: "pdf", path: @"c:\...\document.pdf");

var messageToPrint = new PrintMessage(sender, printDetails, primaryDocument)

var result = client.SendMessage(messageToPrint);
// MessageStatus.DeliveredToPrint

Send letter with higher security level

var primaryDocument = new Document(subject: "Primary document", fileType: "pdf", path: @"c:\...\document.pdf")
{
    AuthenticationLevel = AuthenticationLevel.TwoFactor, // Require BankID or BuyPass to open letter
    SensitivityLevel = SensitivityLevel.Sensitive // Sender information and subject will be hidden until Digipost user is logged in at the appropriate authentication level
};

var message = new Message(
    sender,
    new RecipientById(identificationType: IdentificationType.PersonalIdentificationNumber, id: "311084xxxx"),
    primaryDocument
);

var result = client.SendMessage(message);

Send letter with higher security level

var config = new ClientConfig("xxxxx", Environment.Production);
var client = new DigipostClient(config, thumbprint: "84e492a972b7e...");

var primaryDocument = new Document(subject: "Primary document", fileType: "pdf", path: @"c:\...\document.pdf");

primaryDocument.AuthenticationLevel = AuthenticationLevel.TwoFactor; // Require BankID or BuyPass to open letter
primaryDocument.SensitivityLevel = SensitivityLevel.Sensitive; // Sender information and subject will be hidden until Digipost user is logged in at the appropriate authentication level

var message = new Message(
    new RecipientById(identificationType: IdentificationType.PersonalIdentificationNumber, id: "311084xxxx"), primaryDocument);

var result = client.SendMessage(message);

Identify recipient

Attempts to identify the person submitted in the request and returns whether he or she has a Digipost account. The person can be identified by personal identification number (PIN), Digipost address, or name and address.

If the user is identified, the ResultType will be DigipostAddress or PersonAlias. In cases where we identify the person, the data parameter will contain the given identification value.

User is identified and have a Digipost account:

ResultType: DigipostAddress
Data: "Ola.Nordmann#3244B"
Error: Null

User is identified but does not have a Digipost account:

ResultType: PersonAlias
Data: "azdixsdfsdffsdfncixtvpwdp#6QE6"
Error: Null

The PersonAlias can be used for feature lookups, instead of the given identify criteria.

Note: If you identify a person by PIN, the Digipost address will not be returned, since it is preferred that you continue to use this as identificator when sending the message.

If the user is not identified, the ResultType will be InvalidReason or UnidentifiedReason. See the Error parameter for more detailed error message.

The user is not identified because the PIN is not valid:

ResultType: InvalidReason
Data: Null
Error: InvalidPersonalIdentificationNumber

The user is not identified because we did not have a match from the identify criteria:

ResultType: UnidentifiedReason
Data: Null
Error: NotFound

Following is a example that uses personal identification number as identification choice.

var identification = new Identification(new RecipientById(IdentificationType.PersonalIdentificationNumber, "211084xxxxx"));
var identificationResponse = client.Identify(identification);

if (identificationResponse.ResultType == IdentificationResultType.DigipostAddress)
{
    //Exist as user in Digipost.
    //If you used personal identification number to identify - use this to send a message to this individual.
    //If not, see Data field for DigipostAddress.
}
else if (identificationResponse.ResultType == IdentificationResultType.Personalias)
{
    //The person is identified but does not have an active Digipost account.
    //You can continue to use this alias to check the status of the user in future calls.
}
else if (identificationResponse.ResultType == IdentificationResultType.InvalidReason ||
         identificationResponse.ResultType == IdentificationResultType.UnidentifiedReason)
{
    //The person is NOT identified. Check Error for more details.
}

Send letter through Norsk Helsenett

The Digipost API is accessible from both internet and Norsk Helsenett (NHN). Both entry points use the same API, the only difference is that ClientConfig.Environment must be set to Environment.NorskHelsenett.

// API URL is different when request is sent from NHN
var config = new ClientConfig(new Broker(12345), Environment.NorskHelsenett);
var client = new DigipostClient(config, thumbprint: "84e492a972b7e...");

var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt")
);

var result = client.SendMessage(message);

Search for receivers

A central part of a user interface in the application that is integrating with Digipost is the possiblity to search for receivers. This is available via the search endpoint. A person can be found by simply searching by first name and last name, e.g. Ola Nordmann, or specified further by street address, postal code, city and organization name.

It is important to note that the search results returned do not necessarily include the receiver to which you actually wish to send. The search results returned are strictly based on the search query you have sent in. This equally applies when only one search result is returned. This means that the actual person to which you wish to send must be confirmed by a human being before the actual document i sent (typically in the senders application). If the goal is to create a 100% automated workflow then the identify recipient endpoint should be used (see Identify recipient use case).

var response = client.Search("Ola Nordmann Bellsund Longyearbyen");

foreach (var person in response.PersonDetails)
{
    var digipostAddress = person.DigipostAddress;
    var phoneNumber = person.MobileNumber;
}

Send on behalf of organization

In the following use case, Sender is defined as the party who is responsible for the actual content of the letter. Broker is defined as the party who is responsible for the technical transaction, which in this context means creating the request and being the party that is authenticated.

example

Sending on behalf of an organization is accomplished by setting Message.Sender to the id of the sender when constructing a message. The actual letter will appear in the receivers Digipost mailbox with the senders details (logo, name, etc.).

Remember to use the enterprise certificate of the broker to sign the message, not the one belonging to the sender. Also, the proper permissions need to be set by Digipost to send on behalf of an organization.

Let us illustrate this with an example. Let BrokerCompany be an organization with id 12345, and thumbprint of their certificate 84e492a972b7e…. They want to send on behalf of SenderCompany with organization id 67890.

var broker = new Broker(12345);
var sender = new Sender(67890);

var digitalRecipient = new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx");
var primaryDocument = new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt");


var clientConfig = new ClientConfig(broker, Environment.Production);

var message = new Message(sender, digitalRecipient, primaryDocument);

var result = client.SendMessage(message);

Send message with delivery time

A message can be sent with a delivery time. This means that a message can be sent at 11 AM, and be delivered to the recipient’s Digipost inbox at 3 PM the next day. Message.DeliveryTime is used for this purpose and is of type DateTime. This gives you a lot of flexibility on how to set the delivery time.

var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt")
)
{
    DeliveryTime = DateTime.Now.AddDays(1).AddHours(4)
};

var result = client.SendMessage(message);

Get status of a sent document

You can for any given document check its status to see if it and when has been delivered, status of read approval, delivery method etc.

To do this you need to have the Guid given to the document.

Send the message:

var documentGuid = Guid.NewGuid();
var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt")
    {
        Guid = documentGuid.ToString()
    }
);

client.SendMessage(message);

To fetch fhe DocumentStatus later:

var documentStatus = _digipostClient.GetDocumentStatus(sender).GetDocumentStatus(documentGuid).Result;

This can be useful if you use fallback to print, print-if-unread, request for registration etc.

Get Sender by organisation number and a part id and send a message

In very specific usecases where a broker organisation has multiple sub-organisations in Digipost it is possible to send with organisation number and a partid. The partid can be used to distinguish between different divisions or however the organisation sees fit. This makes it possible to not have to store the Digipost account id, but in stead fetch this information from the api.

Fetch sender information based on orgnumber and partid.

var senderInformation = client.GetSenderInformation(new SenderOrganisation("9876543210", "thePartId"));

var message = new Message(
    senderInformation.Sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "txt", path: @"c:\...\document.txt")
);

var result = client.SendMessage(message);

Get document events

Document events is special events created by Digipost when certain events happens to a document that the broker/sender could use to perform other operations with regards to the processing of the document.

When a sender send a RequestForRegistration one might be interested in knowing IF the user has registered with in the timeout specified to for instance print the message or do som other processing. When the timeout is reached, Digipost will create a DocumentEvent with type RequestForRegistrationExpired. This can then be fetched by the sender via DocumentEvent-api.

This is done by polling in certain intervals defined by the sender.

The following code fetches at max 100 events last 5 minutes.

var from = DateTime.Now.Subtract(TimeSpan.FromMinutes(5));
var to = DateTime.Now;

DocumentEvents events = await Client.DocumentsApi(Sender)
                .GetDocumentEvents(from, to, offset: 0, maxResults: 100);

IEnumerable<DocumentEventType> documentEventTypes = events.Select(aEvent => aEvent.EventType);

Please note that this does not mean that you return all events in the time interval, but the maxResult you ask for. This means that if you get, say, 100 events in the list back, there is a possibility that there are more events in the specified time interval. You then need to fetch the same interval again, but specify an offset according to you maxResult.

Note that we here specify offset as 100 and use the same DateTime instance as used above.

DocumentEvents events = await Client.DocumentsApi(Sender)
                .GetDocumentEvents(from: from, to: to, offset: 100, maxResults: 100);

IEnumerable<DocumentEventType> documentEventTypes = events.Select(aEvent => aEvent.EventType);

Usually we want the you to pull as few events at a time that seems reasonable since you need to process the events somehow while pulling more.

When you receive 0 og less that the specified maxResults, you have received all events in the current time interval.

We propose that you somehow store the state of the polling time interval. We expire/delete events after 30 days, so there are enough time to be sure you have processed them all. There is no way to delete an event through the api.

Send smart post

Send a message with extra computer readable data

Starting with version 8 of the Digipost API, messages can have extra bits of computer readable information that allows the creation of a customized, dynamic user experience for messages in Digipost. These extra bits of information are referred to as “Datatypes”.

All datatypes are sent in the same way. Each document can accommodate one datatype-object. An exhaustive list of available datatypes and their documentation can be found at digipost/digipost-data-types-dotnet.

Send invoice or inkasso (Dept collection)

It is possible to send invoice-metadata with a document. Documents with invoice-metadata enables the Send to Online Bank feature, which allows people to pay the invoice directly in Digipost.

var invoice = new Invoice(dueDate: DateTime.Parse("2022-12-03T10:15:30+01:00 Europe/Paris"), sum: new decimal(100.21), creditorAccount: "2593143xxxx")
{
    Kid = "123123123"
};

var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "211084xxxx"),
    new Document(
        subject: "Invoice 1",
        fileType: "pdf",
        path: @"c:\...\invoice.pdf",
        dataType: invoice)
);

var result = client.SendMessage(message);

It is possible to send inkasso-metadata with a document.

var inkasso = new Inkasso(dueDate: DateTime.Parse("2022-12-03T10:15:30+01:00 Europe/Paris"))
{
    Kid = "123123123",
    Sum = new decimal(100.21),
    Account = "2593143xxxx"
};

var message = new Message(
    sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "211084xxxx"),
    new Document(
        subject: "Invoice 1",
        fileType: "pdf",
        path: @"c:\...\invoice.pdf",
        dataType: inkasso)
);

var result = client.SendMessage(message);

Send message with appointment datatype

Appointment represents a meeting set for a specific place and time. The following example demonstrates how to include such extra data:

var appointment = new Appointment(startTime: DateTime.Parse("2017-11-24T13:00:00+0100"))
{
    EndTime = DateTime.Parse("2017-11-24T13:00:00+0100").AddMinutes(30),
    Address = new Address {StreetAddress = "Storgata 1", PostalCode = "0001", City = "Oslo"}
};

var document = new Document(
    subject: "Your appointment",
    fileType: "pdf",
    path: @"c:\...\document.pdf",
    dataType: appointment
);

// Create Message and send using the client as specified in other examples.

ExternalLink enhances a message in Digipost with a button which sends the user to an external site. The button can optionally have a deadline, a description and a custom text.

var externalLink = new ExternalLink(absoluteUri: new Uri("https://example.org/loan-offer/uniqueCustomerId/"))
{
    Description = "Please read the terms, and use the button above to accept them. The offer expires at 23/10-2018 10:00.",
    ButtonText = "Accept offer",
    Deadline = DateTime.Parse("2018-10-23T10:00:00+0200")
};

var document = new Document(
    subject: "Your appointment",
    fileType: "pdf",
    path: @"c:\...\document.pdf",
    dataType: externalLink
);

// Create Message and send using the client as specified in other examples.

Datatype ShareDocumentsRequest

This datatype facilitates exchange of documents between an organisation and a Digipost end user. The sender first sends a message of datatype ShareDocumentsRequest, to which the end user can attach a list of documents. When new documents are shared, a DocumentEvent is generated. The organisation can retrieve the status of their ShareDocumentsRequest. If documents are shared and the sharing is not cancelled, the documents can either be downloaded or viewed on the digipostdata.no domain. Active requests can be cancelled both by the end user and the organisation.

The purpose attribute of the ShareDocumentsRequest should briefly explain why the sender organisation want to gain access to the relevant documents. The primary document should contain a more detailed explanation.

Send ShareDocumentsRequest

var shareDocReq = new ShareDocumentsRequest(
    maxShareDurationSeconds: 60 * 60 * 24 * 5,  // Five calendar days
    purpose: "The purpose for my use of the document");

var requestGuid = Guid.NewGuid(); // Keep this in your database as reference to this particular user interaction

var document = new Document(
    subject: "Information about document sharing",
    fileType: "pdf",
    path: @"c:\...\document.pdf",
    dataType: shareDocReq
)
{
    Guid = requestGuid.ToString()
};

var message = new Message(
    senderInformation.Sender,
    new RecipientById(IdentificationType.PersonalIdentificationNumber, "311084xxxx"),
    new Document(subject: "Attachment", fileType: "pdf", path: @"c:\...\attachment.pdf")
);

IMessageDeliveryResult result = client.SendMessage(message);

Be notified of new shared documents

The sender organisation can be notified of new shared documents by polling document events regularly. Use the uuid attribute of the DocumentEvent to match with the messageUUID of the origin ShareDocumentsRequest.

This particular api is still not developed for .NET client. Periodically check the below api to check current status for sent shared request.

Get state of ShareDocumentsRequest

  • Use the requestGuid that you stored from previous.
var shareDocumentsRequestState = await client.GetDocumentSharing(sender)
                .GetShareDocumentsRequestState(requestGuid);

If the shareDocumentsRequestState.SharedDocuments.Count() > 0 is true, then the user has shared a document for this share request.

Get documents

Each SharedDocument has attributes describing the document and its origin. If SharedDocumentOrigin is of type OrganisationOrigin, the corresponding document was received by the end user through Digipost from the organisation with the provided organisation number. If the origin is of type PrivatePersonOrigin, the document was received either from another end user or uploaded by the user itself.

Get a single document as stream:

Stream stream = await client.GetDocumentSharing(sender)
    .FetchSharedDocument(
        shareDocumentsRequestState.SharedDocuments.First().GetSharedDocumentContentStreamUri()
    );

Get link to view a single document on digipostdata.no:

SharedDocumentContent sharedDocumentContent = await client.GetDocumentSharing(sender)
    .GetShareDocumentContent(
        shareDocumentsRequestState.SharedDocuments.First().GetSharedDocumentContentUri()
    );

Uri uriToOpenInBrowser = sharedDocumentContent.Uri;

Stop sharing

Use the requestGuid from before. Top stop the sharing you add more information on the original message you sent to start this process. Stop sharing can be done when you definitely don’t need the document to be shared any more. The sharing will in any case stop at the originally specified max share duration automatically.

var shareDocumentsRequestState = await client.GetDocumentSharing(sender)
                .GetShareDocumentsRequestState(requestGuid);

var additionalData = new AdditionalData(sender, new ShareDocumentsRequestSharingStopped());

client.AddAdditionalData(additionalData, shareDocumentsRequestState.GetStopSharingUri());

Receive messages

Client configuration

ClientConfig is a container for all the connection specific paramters that you can set.

// The actual sender of the message. The broker is the owner of the organization certificate 
// used in the library. The broker id can be retrieved from your Digipost organization account.
const int brokerId = 12345;
            
var broker = new Broker(brokerId);
var clientConfig = new ClientConfig(broker, Environment.Production);

var client = new DigipostClient(clientConfig, CertificateReader.ReadCertificate());

Get documents inbox

The inbox call outputs a list of documents ordered by DeliveryTime. Offset is the start index of the list, and limit is the max number of documents to be returned. The offset and limit are therefore not in any way connected to InboxDocument.Id.

The values offset and limit are meant for pagination so that one can fetch 100 and then the next 100.

var inbox = client.GetInbox(sender);

var first100 = inbox.Fetch(); //Default offset is 0 and default limit is 100

var next100 = inbox.Fetch(offset: 100, limit: 100);

We have now fetched the 200 newest inbox documents. As long as no new documents are received, the two API-calls shown above will always return the same result. If we now receive a new document, this will change. The first 100 will now contain 1 new document and 99 documents we have seen before. This means that as soon as you stumble upon a document you have seen before you can stop processing, given that all the following older ones have been processed.

Download document content

var inbox = client.GetInbox(sender);

var documentMetadata = (await inbox.Fetch()).First();

var documentStream = await inbox.FetchDocument(documentMetadata.GetGetDocumentContentUri());

Delete document

var inbox = client.GetInbox(sender);

var documentMetadata = (await inbox.Fetch()).First();

await inbox.DeleteDocument(documentMetadata.GetDeleteUri());

Archive

The archive API makes it possible for an organisation to manage documents in archives. These files are kept in separate archives, and the files belong to the sender organisation.

Archive a file

Let’s say you want to archive two documents eg. an invoice and an attachment and you want to have some kind of reference to both documents. You can do that by describing the two documents with ArchiveDocument. Then you need to create an archive and add the documents to the archive. In the following example we use a default archive. readFileFromDisk is just made up function to showcase how you might read a file to a byte[] sender is your sender, the one you use to create the client instance, if you are not a broker. For broker see below.

var archive = new Archive.Archive(sender, new List<ArchiveDocument>
{
    new ArchiveDocument(Guid.NewGuid(), "invoice_123123.pdf", "pdf","application/psd", readFileFromDisk("invoice_123123.pdf")),
    new ArchiveDocument(Guid.NewGuid(), "attachment_123123.pdf", "pdf","application/psd", readFileFromDisk("attachment_123123.pdf"))
});

var savedArchive = await client.GetArchive().ArchiveDocuments(archive);

You can name the archive if you wish to model your data in separate archives

var archive = new Archive.Archive(sender, new List<ArchiveDocument>(), "MyArchiveName");

Get a list of archives

An organisation can have many archives, or just the default unnamed archive. That is up to your design wishes. To get a list of the archives for a given Sender, you can do this:

IEnumerable<Archive.Archive> fetchedArchives = await client.GetArchive().FetchArchives();

Iterate documents in an archive

You can get content of an archive with paged requests. Under is an example of how to iterate an archive. However, its use is strongly discouraged because it leads to the idea that an archive can be iterated. We expect an archive to possibly reach many million rows so the iteration will possibly give huge loads. On the other hand being able to dump all data is a necessary feature of any archive.

Please use fetch document by UUID or referenceID instead to create functionality on top of the archive. You should on your side know where and how to get a document from an archive. You do this by knowing where you put a file you want to retrieve.

var current = (await client.GetArchive().FetchArchives()).First();
var documents = new List<ArchiveDocument>();

while (current.HasMoreDocuments())
{
    var fetchArchiveDocuments = client.GetArchive().FetchArchiveDocuments(current.GetNextDocumentsUri()).Result;
    documents.AddRange(fetchArchiveDocuments.ArchiveDocuments);
    current = fetchArchiveDocuments;
}

// documents now have all ArchiveDocuments in the archive (not the actual bytes, just the meta data)

In c# 8 there is a language feature to produce a IAsyncEnumerable so that you can implement a stream of archive documents. But this has not been implemented in this client library since we are on dotnetstandard2.0. For each iteration above you will get 100 documents.

Archive Document attributes

You can add optional attributes to documents. An attribute is a key/val string-dictionary that describe documents. You can add up to 15 attributes per archive document. The attribute’s key and value are case sensitive.

var archiveDocument = new ArchiveDocument(Guid.NewGuid(), "invoice_123123.pdf", "pdf", "application/psd", readFileFromDisk("invoice_123123.pdf"))
{
    Attributes = {["invoicenumber"] = "123123"}
};

The attributes can be queried, so that you can get an iterable list of documents.

var archive = (await client.GetArchive().FetchArchives()).First();
var searchBy = new Dictionary<string, string>
{
    ["key"] = "val"
};

var fetchArchiveDocuments = client.GetArchive().FetchArchiveDocuments(archive.GetNextDocumentsUri(searchBy)).Result;
var documents = fetchArchiveDocuments.ArchiveDocuments;

// documents now have all ArchiveDocuments in the archive that has invoicenumber=123123

We recommend that the usage of attributes is made such that the number of results for a query on attributes is less than 100. If you still want that, it’s ok, but you need to iterate the pages to get all the results like we did in the example where we fetch all documents in an archive.

var current = (await client.GetArchive().FetchArchives()).First();
var documents = new List<ArchiveDocument>();

var searchBy = new Dictionary<string, string>
{
    ["key"] = "val"
};

while (current.HasMoreDocuments())
{
    var fetchArchiveDocuments = client.GetArchive().FetchArchiveDocuments(current.GetNextDocumentsUri(searchBy)).Result;
    documents.AddRange(fetchArchiveDocuments.ArchiveDocuments);
    current = fetchArchiveDocuments;
}

// documents now have all ArchiveDocuments in the archive that has invoicenumber=123123

Get documents by referenceID

You can retrieve a set of documents by a given referenceID. You will then get the documents listed in their respective archives in return. ReferenceId is a string you can add to documents. The idea is that you might want to model documents in different archives and we able to fetch them combined. A good reference ID might be a unique technical process ID or a conversation ID of some kind. It is thus not meant to describe the actual document but is rather a join key of some kind.

IEnumerable<Archive.Archive> fetchArchiveDocumentsByReferenceId = await client.GetArchive().FetchArchiveDocumentsByReferenceId("MyProcessId[No12341234]");

As you can see, it is a list of Archives containing ArchiveDocument; You should choose a ReferenceId in such a way to try and keep the count of documents low so as feasible.

Get documents by externalId or Guid/Uuid

You can retrieve a set of documents by the Guid/UUID that you give the document when you archive it. In the example above we use Guid.NewGuid() to generate an uuid/guid. You can either store that random uuid in your database for retrieval later, or you can generate a deterministic uuid based on your conventions for later retrieval.

You will get in return an instance of Archive which contains information on the archive the document is contained in and the actual document. From this you can fetch the actual document.

ArchiveDocument archiveDocument = (await client.GetArchive(sender).FetchDocumentFromExternalId(Guid.Parse("10ff4c99-8560-4741-83f0-1093dc4deb1c"))).One();
Archive archive = await client.GetArchive(sender).FetchDocumentFromExternalId("MyExternalId");

You can store your guid that you used at upload time to fetch by Guid.

Note: The reason for having the string and Guid as a parameter is that with the java client it is possible to create an UUID from a string. This is done deterministically. Under normal circumstance you should use Guid.NewGuid() when you upload an document and store this id for reference in your system. But if you want to fetch a document that you know is stored by another entity that uses the deterministic UUID approach with java and all you have is the original string, then you need to use the string parameterized method. Java generates UUID in slightly different manner compared to .NET Guid. Therefore we have implemented the same UUID-generation logic into the client library api. You can use the method Guid.Parse(UuidInterop.NameUuidFromBytes("MyExternalId"")) to create an exact equal value for a Guid compared to Uuid.

You can get the actual content of a document after you have retrieved the archive document. Below is an example of how you can achieve this with a given ArchiveDocument. In the resulting ArchiveDocumentContent, you will get a url to the content which expires after 30 seconds.

//First fetch the archiveDocument
ArchiveDocument archiveDocument = await client.GetArchive(sender).FetchArchiveDocument(client.GetRoot(new ApiRootUri()).GetGetArchiveDocumentsByUuidUri(Guid.Parse("10ff4c99-8560-4741-83f0-1093dc4deb1c")));

ArchiveDocumentContent archiveDocumentContent = await client.GetArchive().GetDocumentContent(archiveDocument.DocumentContentUri());
Uri uri = archiveDocumentContent.Uri;

Get content of a document as a stream

In addition to a single-use link, you also have the option to retrieve the content of a document directly as a byte stream.

Stream streamDocumentFromExternalId = await client.GetArchive(sender).StreamDocumentFromExternalId(Guid.Parse("10ff4c99-8560-4741-83f0-1093dc4deb1c"));

Update document attributes and/or referenceId

You can add an attribute or change an attribute value, but not delete an attribute. You can however set the value to empty string. The value of the field for referenceID can be changed as well.

ArchiveDocument archiveDocument = await client.GetArchive(sender).FetchDocumentFromExternalId(Guid.Parse("10ff4c99-8560-4741-83f0-1093dc4deb1c"));
archiveDocument.WithAttribute("newKey", "foobar")
    .WithReferenceId("MyProcessId[No12341234]Done");

ArchiveDocument updatedArchiveDocument = await client.GetArchive().UpdateDocument(archiveDocument, archiveDocument.GetUpdateUri());

Set autoDelete time

Sometimes you want to delete documents automatically. This can be done at upload time.

var archive = new Archive.Archive(sender, new List<ArchiveDocument>
{
    new ArchiveDocument(Guid.NewGuid(), "invoice_123123.pdf", "pdf", "application/psd", readFileFromDisk("invoice_123123.pdf"))
        .WithDeletionTime(DateTime.Today.AddYears(5))
});

var savedArchive = await client.GetArchive().ArchiveDocuments(archive);

Using archive as a broker

It is possible to be a broker for an actual sender. Most of the api described above also support the use of SenderId to specify who you are archiving for.

eg.:

client.GetArchive(new Sender(111111)).FetchArchives()

As you can see, this is typically done when you get the api part GetArchive. This will use the feature of Root element in the restful api and serve the client with url resources specific for that sender you are broker for.

At upload time, however, this is done in the actual message you send.

var archive = new Archive.Archive(new Sender(111111), new List<ArchiveDocument>
{
    new ArchiveDocument(Guid.NewGuid(), "invoice_123123.pdf", "pdf","application/psd", readFileFromDisk("invoice_123123.pdf")),
    new ArchiveDocument(Guid.NewGuid(), "attachment_123123.pdf", "pdf","application/psd", readFileFromDisk("attachment_123123.pdf"))
});

var savedArchive = await client.GetArchive().ArchiveDocuments(archive);

Logging

Debugging

Enabling Logging

The client library has the ability to log useful information that can be used for debug purposes. To enable logging, supply the DigipostClient with an implementation of Microsoft.Extensions.Logging.ILoggerFactory. This is Microsoft’s own logging API, and allows the user to chose their own logging framework.

Enabling logging on level DEBUG will output positive results of requests and worse, WARN only failed requests or worse, while ERROR will only occur on failed requests. These loggers will be under the Digipost.Api.Client namespace.

Implementing using NLog

There are numerous ways to implement a logger, but the following examples will be based on NLog documentation.

  1. Install the Nuget-packages NLog, NLog.Extensions.Logging and Microsoft.Extensions.DependencyInjection.
  2. Create an nlog.config file. The following is an example that logs to both file and console:
<?xml version="1.0" encoding="utf-8"?>

<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogFile="c:\temp\console-example-internal.log"
      internalLogLevel="Info">
    <!-- the targets to write to -->
    <targets>
        <!-- write logs to file -->
        <target xsi:type="File"
                name="fileTarget"
                fileName="${specialfolder:folder=UserProfile}/logs/digipost-api-client-dotnet/digipost-api-client-dotnet.log"
                layout="${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}"
                archiveEvery="Day"
                archiveNumbering="Date"
                archiveDateFormat="yyyy-MM-dd"/>
        <target xsi:type="Console"
                name="consoleTarget"
                layout="${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}" />
    </targets>

    <!-- rules to map from logger name to target -->
    <rules>
        <logger name="*" minlevel="Trace" writeTo="fileTarget,consoleTarget"/>
    </rules>
</nlog>

In your application, do the following to create a logger and supply it to DigipostClient:

private static IServiceProvider CreateServiceProviderAndSetUpLogging()
{
    var services = new ServiceCollection();

    services.AddSingleton<ILoggerFactory, LoggerFactory>();
    services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
    services.AddLogging((builder) => builder.SetMinimumLevel(LogLevel.Trace));

    var serviceProvider = services.BuildServiceProvider();
    SetUpLoggingForTesting(serviceProvider);

    return serviceProvider;
}

private static void SetUpLoggingForTesting(IServiceProvider serviceProvider)
{
    var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

    loggerFactory.AddNLog(new NLogProviderOptions {CaptureMessageTemplates = true, CaptureMessageProperties = true});
    NLog.LogManager.LoadConfiguration("./nlog.config");
}

static void Main(string[] args)
{
    ClientConfig clientConfig = null;
    
    var serviceProvider = CreateServiceProviderAndSetUpLogging();
    var client = new DigipostClient(clientConfig, CertificateReader.ReadCertificate(), serviceProvider.GetService<ILoggerFactory>());
}

Request and Response Logging

For initial integration and debugging purposes, it can be useful to log the actual request and response going over the wire. This can be enabled by doing the following:

Set the property ClientConfig.LogRequestAndResponse = true.

Warning: Enabling request logging should never be used in a production system. It will severely impact the performance of the client.

Error Handling

Error Handling

If you are communicating synchronously, the errors will be wrapped in an AggregateException. This is because a set of errors may have occured before you receive You can handle each exception within the aggregate by sending in an anonymous function to AggregateException.Handle():

try
{
    var messageDeliveryResult = api.SendMessage(message);	
}
catch (AggregateException ae)
{
    ae.Handle((x) =>
    {
        if (x is ClientResponseException)
        {
            Console.WriteLine("A client response exception occured!");
            return true;
        }
        return false;
    });
}

If you are using methods in the client library that has async in the name, it means that you are communicating asynchronously. To correctly identify errors that might happen, for example during sending a message, you can catch errors like you normally would in an application:

try
{
    var messageDeliveryResult = await api.SendMessageAsync(message);	
}
catch(ClientResponseException e)
{
    Console.WriteLine("A client response exception occured!");
}

Asynchronous communication with Digipost using the client library involves using methods with async in method name, like SendMessageAsync or IdentifyAsync.