Unofficial API client implementation for the Pingen v2 API. See API Doc. (used version 2.0.0)
With a special thanks to Pingen GmbH for the handy Online Postal Service for sending letters digitally. See Pingen website.
- PingenApiNet: Client service to interact with the Pingen v2 API
- PingenApiNet.Abstractions: Models, Views and Enums used for the API
- PingenApiNet.AspNetCore: Dependency injection in ASP.NET Core
Get a file path and upload file.
var resultFilePath = await _pingenApiClient.Files.GetPath();
if (!resultFilePath.IsSuccess)
return $"Failed to get file upload path from pingen: {JsonConvert.SerializeObject(resultFilePath.ApiError)}";
if (resultFilePath.Data?.Data is null)
return "Failed to get file upload path from pingen, request was OK but return null";
if (!await _pingenApiClient.Files.UploadFile(resultFilePath.Data.Data, contentStream))
return "Failed to upload file to pingen storage";
Create the letter.
var resultCreateLetter = await _pingenApiClient.Letters.Create(new()
{
Attributes = new()
{
FileOriginalName = filename,
FileUrl = resultFilePath.Data.Data.Attributes.Url,
FileUrlSignature = resultFilePath.Data.Data.Attributes.UrlSignature,
AddressPosition = LetterAddressPosition.left,
AutoSend = false,
DeliveryProduct = LetterCreateDeliveryProduct.Cheap, // A more specific product can be used later at Letters/Send endpoint
PrintMode = LetterPrintMode.simplex,
PrintSpectrum = LetterPrintSpectrum.color,
MetaData = new() // This must only be set when "postag_registered" or "postag_registered" product used. Otherwise the API can fail at address validation when Zip code has more than 4 characters.
{
Recipient = new()
{
Name = recipientName,
Street = recipientAddress.Street,
Number = recipientAddress.HouseNumber,
Zip = recipientAddress.Postcode,
City = recipientAddress.City,
Country = recipientAddress.Country
},
Sender = new()
{
Name = senderName,
//...
Country = senderAddress.Country
}
}
},
Type = PingenApiDataType.letters
}, $"Letters.Create.{outgoing.Id}"); // Optionally, idempotency key
if (!resultCreateLetter.IsSuccess)
return $"Failed create letter on pingen: {JsonConvert.SerializeObject(resultCreateLetter.ApiError)}";
if (resultCreateLetter.Data?.Data is null)
return "Failed create letter on pingen, request was OK but return null";
Check the letter state and send it.
// NOTE: Consider using a loop with delay to poll the letter state (Pingen needs some seconds to validate the letter)
var resultGetLetter = await _pingenApiClient.Letters.Get(letter.Id);
if (resultGetLetter.IsSuccess
&& resultGetLetter.Data?.Data.Attributes.Status == LetterStates.Valid)
{
var resultSendLetter = await _pingenApiClient.Letters.Send(new()
{
Type = PingenApiDataType.letters,
Attributes = new()
{
DeliveryProduct = LetterSendDeliveryProduct.PostAgA
PrintMode = LetterPrintMode.simplex,
PrintSpectrum = LetterPrintSpectrum.color,
MetaData = // Use from sample above at Letters.Create
},
Id = resultGetLetter.Data!.Data.Id
});
if (!resultSendLetter.IsSuccess)
return $"Failed to send letter on pingen: {JsonConvert.SerializeObject(resultSendLetter.ApiError)}";
}
Get letter and download file.
var resultFileLocation = await _pingenApiClient.Letters.GetFileLocation(letterId);
if (!resultFileLocation.IsSuccess)
return new(null, $"Failed to get file location from letter on pingen: {JsonConvert.SerializeObject(resultFileLocation.ApiError)}");
if (resultFileLocation.Location is null)
return new(null, "Failed to get file location from letter on pingen, request was OK but location null");
try
{
var memoryStrem = await _pingenApiClient.Letters.DownloadFileContent(resultFileLocation.Location);
}
catch (PingenFileDownloadException e)
{
return $"Failed to download file from pingen: {e.ErrorCode}";
}
Get all delivery products with auto paging.
await foreach (var page in _pingenApiClient.Distributions.GetDeliveryProductsPageResultsAsync(apiPagingRequest))
{
deliveryProducts.AddRange(page);
}
This is barely tested but should work as follows:
var apiPagingRequest = new ApiPagingRequest
{
Sorting = new Dictionary<string, CollectionSortDirection>
{
[PingenAttributesPropertyHelper<Letter>.GetJsonPropertyName(letter => letter.CreatedAt)] = CollectionSortDirection.DESC
},
Filtering = new(
CollectionFilterOperator.And,
new KeyValuePair<string, object>[]
{
new(CollectionFilterOperator.Or, new KeyValuePair<string, object>[]
{
new(PingenAttributesPropertyHelper<Letter>.GetJsonPropertyName(letter => letter.Country), "CH"),
new(PingenAttributesPropertyHelper<Letter>.GetJsonPropertyName(letter => letter.Country), "LI")
}),
new(PingenAttributesPropertyHelper<Letter>.GetJsonPropertyName(letter => letter.Status), "valid")
})
};
Register pingen services. You can inject the client by interface IPingenApiClient.
services.AddPingenServices(new PingenConfiguration
{
BaseUri = "https://api-staging.pingen.com",
IdentityUri = "https://identity-staging.pingen.com",
ClientId = /****/,
ClientSecret = /****/,
DefaultOrganisationId = /****/
});
Validate webhook and extract data.
[NonAction]
private async Task<(WebhookEventData WebhookEventData, Data<Organisation> OrganisationData, Data<Letter> LetterData, Data<LetterEvent> EventData)> ValidateAndGetWebhookEvent()
{
// Enable request buffering for multiple body stream reads
Request.EnableBuffering();
// Ensure signature header received
if (!Request.Headers.TryGetValue("Signature", out var signature))
throw new InvalidOperationException("Missing 'Signature' header");
// Validate webhook body and get data
var (webhookEventData, organisationData, letterData, eventData) = await PingenWebhookHelper.ValidateWebhookAndGetData(signingKey, /*your signature*/, Request.Body);
if (webhookEventData is null)
throw new InvalidOperationException("Failed to parse WebhookEventData from body, resulted as NULL");
return (webhookEventData, organisationData, letterData, eventData);
}