inema.rest module¶
Deutsche Post Internetmarke REST API client.
Deutsche Post released its Internetmarke REST API in September, 2024, as a successor to its Internetmarke SOAP API, which it deprecated in August, 2024 and announced to turn it off at the end of 2025.
For sending postage requests to Deutsche Post a Session object is required.
The body parameter that is necessary for some requests can be constructed with the
mk_* functions.
For the actual checkout the total price need to be supplied and the checkout only succeeds
if your local computation matches the remote one. See also the calc_total() and compute_total() helper functions.
Example
The following snippets buys a registered mail postage PDF label that can be directly printed to a C6 envelope:
import inema.rest as ir
im = ir.Session('some client id', 'some client secret',
'some portokasse user', 'some portokasse password')
oid = im.create_order()
p = ir.mk_pdf_pos(1002,
sender=ir.mk_addr('Erika Mustermann', 'Heidestr. 17', '51147', 'Köln'),
receiver=ir.mk_addr('Bernd Müller', 'Nußhäherstraße 10', '12345', 'München') )
t = ir.calc_total(p)
body = ir.mk_pdf_req(oid, 5, p, t)
d = im.checkout_pdf(body, 'postage.pdf')
Alternatively, it’s also possible to check out a list of label positions, e.g. to create multiple pages or when printing multiple labels in columns and rows to one page.
Note
The new Internetmarke REST API is structured quite similarly to the previous SOAP API. On the one hand this might simplify migration, because the workflow is basically the same, but on the other hand the REST API aguably is non-idiomatic and contains too many surprises such that it’s harder to adopt than necessary.
Hint
This package contains default Deutsche Post prices, but some customers may have discounts in place.
Important
A fresh ‘Portokasse’ needs to allow-list the ‘application’ that is identified by the client id. This can be accomplished most easily by a first session login attempt that will fail, but also trigger an authorization request that can be acknowledged in the ‘Portokasse’ account web UI, after the fact.
Error
This module uses the requests package for all HTTP requests.
Consequently, REST API HTTP error status codes are reported via raised
requests.exceptions.HTTPError exceptions.
When caught, the remote error may be obtained like this:
try:
Session im(client, secret, user, password)
// some requests, checkout, whatever ...
except requests.exceptions.HTTPError as e:
d = e.response.json()
print(f'Remote errror: {d}')
Examples of such an error:
{'statusCode': '400',
'title': 'Bad Request',
'description': 'positions[4].position.labelX: must be greater than or equal to 1',
'instance': 'PCF-A1033'}
Another error example:
{'statusCode': '400',
'title': 'invalidTotalAmount',
'description': 'The total amount of the order is invalid! Reason: The total cost of the shopping cart is invalid! PPL-total: 415, Cart: 1',
'instance': 'PCF-A1031'}
- class inema.rest.Layout(*values)[source]¶
Bases:
StrEnum- ADDRESS_ZONE = 'ADDRESS_ZONE'¶
- FRANKING_ZONE = 'FRANKING_ZONE'¶
- class inema.rest.Session(client, secret, user, password, api_base='https://api-eu.dhl.com/post/de/shipping/im/v1/')[source]¶
Bases:
objectInternetmarke REST API Connection class.
An object of this class primarily wraps the requests http session (ses), the bearer token obtained after authorization (token) and the current balance of the user’s Portokasse wallet (balance).
The constructor authorises to Internetmarke REST API endpoint.
- Parameters:
client (str) – Client ID, a.k.a. the API key credential of your ‘Post DE Internetmarke (Post & Parcel Germany)’ API application that is registered in your developer.dhl.com user account.
secret (str) – Client secret, a.k.a. the API Secret credential of yoru ‘Post DE Internetmarke (Post & Parcel Germany)’ API application that is registered in your developer.dhl.com user account.
user (str) – username of your portokasse.deutschepost.de Portokasse account
password (str) – password of your portokasse.deutschepost.de Portokasse account
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
>>> im = Session im(client, secret, user, password) >>> im.auth_resp {'userToken' : 'seCreTbeArer=', 'access_token' : 'seCreTbeArer=', 'walletBalance' : 3285, 'showTermsAndConditions' : False, 'infoMessage' : '', 'issued_at' : 'Sun, 14 Sep 2025 14:15:18 GMT', 'expires_in' : 86400, 'token_type' : 'BearerToken', 'external_customer_id' : 'juser-0815', 'authenticated_user' : 'myportokasse@example.org'} >>> im.balance 3285 >>> im.api_base 'https://api-eu.dhl.com/post/de/shipping/im/v1/' >>> type(im.ses) requests.sessions.Session
- cancel(body)[source]¶
Request cancellation of previously checked out postage.
- Parameters:
body (dict) – Request body, usually created with
mk_cancel_req().- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
>>> im.cancel(mk_cancel_req(oid)) { 'shopRetoureId': '123456789', 'retoureTransactionId': '23230815' }
- charge(cent)[source]¶
Top up the Portokasse.
- Parameters:
cent (int) – monetary amount to transfer to your ‘Portokasse’ in euro cent units
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
Top up the wallet (Portokasse) with 7 €, at a balance of 85.05 €:
>>> im.charge(700) {'shopOrderId': '123456', 'walletBalance': 9205} >>> im.balance 9205
- checkout_pdf(body, filename=None)[source]¶
Buy PDF labels.
- Parameters:
body (dict) – Request body, usually created with
mk_pdf_req().filename (str or path_like, optional) – Filename for convenience. If not specified, the PDF can alternatively be retrieved from the URL returned in the ‘link’ field, e.g. using the requests session available via the ses object property.
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
>>> im.checkout_pdf(body_with_4_positions, 'labels.pdf') {'type': 'CheckoutShoppingCartAppResponse', 'link': 'https://internetmarke.deutschepost.de/PcfExtensionWeb/document?keyphase=1&data=rANdOMdaTaString%3D', 'manifestLink': None, 'shoppingCart': {'shopOrderId': '123456789', 'voucherList': [{'voucherId': 'A00002300A0000000123', 'trackId': None}, {'voucherId': 'A00002300A0000000122', 'trackId': None}, {'voucherId': 'A00002300A0000000121', 'trackId': None}, {'voucherId': 'A00002300A0000000120', 'trackId': None}]}, 'walletBallance': 4205}
- checkout_png(body, filename=None)[source]¶
Buy PNG labels.
- Parameters:
body (dict) – Request body, usually created with
mk_png_req().filename (str or path_like, optional) – Filename for convenience. NB: The Internetmarke API endpoint always returns PNG labels as ZIP archive, i.e. even when just requesting a single label. If not specified, the ZIP result can alternatively be retrieved from the URL returned in the ‘link’ field, e.g. using the requests session available via the ses object property.
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
- create_order()[source]¶
Initialize new order.
Returns new order ID that is required e.g. for building the body of
checkout_pdf()orcheckout_png().- Returns:
New order ID
- Return type:
str
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
- download(src, filename)[source]¶
Download convenience function.
Downloads through the wrapped requests session, while not wasting too much memory in case the downloaded file is very large.
- Parameters:
src (str) – source URL
filename (str or path_like) – target filename
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
- get_formats()[source]¶
Query format catalogue.
See also
inema.data.formatsor data/formats.json for a cached version of that data.
formats2json()helper to write the result as json
formats2json()write result in a more compact and normalized CSV format
See- Returns:
List of format dictionaries.
- Return type:
list
Example
>>> xs = im.get_formats() >>> xs[0] {'id' : 1, 'isAddressPossible' : True, 'isImagePossible' : False, 'name' : 'DIN A4 Normalpapier', 'description' : None, 'pageType' : 'REGULARPAGE', 'pageLayout' : {'size': {'x': 210, 'y': 297}, 'orientation' : 'PORTRAIT', 'labelSpacing' : {'x': 0, 'y': 0}, 'labelCount' : {'labelX': 2, 'labelY': 5}, 'margin' : {'top': 31, 'bottom': 31, 'left': 15, 'right': 15}}}
- list_cancel(retoure_id=None, transaction_id=None, start=None, end=None)[source]¶
Request status of previously requested cancellations.
- Parameters:
retoure_id (str, optional) – shopRetoureId field previously returned by a
cancel()calltransaction_id (int, optional) – retoureTransactionId field previously returned by a
cancel()callstart (str, optional) – start of query period in ISO 8601 datetime format with
Tdelimiter and without timezoneend (str, optional) – end of query period in ISO 8601 datetime format with
Tdelimiter and without timezone
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
>>> im.list_cancel(start='2025-09-13T23:45:00') [{'retoureTransactionId' : 1234, 'shopRetoureId' : '5678', 'totalCount' : 1, 'countStillOpen' : 0, 'retourePrice' : 415, 'creationDate' : '14092025-143423', 'serialnumber' : 'A00230007A', 'refundedVouchers' : [{'voucherId': 'A00230007A000000042C', 'trackId': None}], 'notRefundedVouchers' : []}, {'retoureTransactionId' : 1233, 'shopRetoureId' : '5677', 'totalCount' : 1, 'countStillOpen' : 0, 'retourePrice' : 215, 'creationDate' : '14092025-142407', 'serialnumber' : 'A00230009A', 'refundedVouchers' : [{'voucherId': 'A00230009A000000023F', 'trackId': None}], 'notRefundedVouchers' : []}]
- list_order(order_id)[source]¶
Request a checked out order.
- Parameters:
order_id (str) – Order ID used in a previous
checkout_pdf()orcheckout_png()call.- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error. Example: When order ID wasn’t used to checkout something, yet.
Example
>>> im.list_order(123456) {'type' : 'CheckoutShoppingCartAppResponse', 'link' : 'https: //internetmarke.deutschepost.de/PcfExtensionWeb/document?keyphase=1&data=blAHblAh%3D', 'manifestLink' : None, 'shoppingCart' : {'shopOrderId': '123456', 'voucherList' : [{'voucherId': 'A00002300A0000000815', 'trackId': None}]}, 'walletBallance' : None}
- preview_pdf(body, filename=None)[source]¶
Request preview PDF.
Note that Deutsche Post doesn’t support previews with addresses. Thus, such previews are only got for testing the QR code placement.
- Parameters:
body (dict) – Request body, usually created with
mk_pdf_preview_req()filename (str or path_like, optional) – Filename for convenience. If not specified, the PDF can alternatively be retrieved from the URL returned in the ‘link’ field, e.g. using the requests session available via the ses object property.
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
Example
>>> im.preview_pdf(mk_pdf_preview_req(5, 1), 'letter.pdf') {'link': 'https://internetmarke.deutschepost.de/PcfExtensionWeb/preview?keyphase=1&data=ranDodmsTringDaTA'}
- preview_png(body, filename=None)[source]¶
Request preview PNG.
Note that Deutsche Post doesn’t support previews with addresses. Thus, such previews are only got for testing the QR code placement.
NB: In contrast to
checkout_png()the API endpoint returns the preview directly as PNG and _not_ inside a ZIP archive.- Parameters:
body (dict) – Request body, usually created with
mk_png_preview_req()filename (str or path_like, optional) – Filename for convenience. If not specified, the PNG can alternatively be retrieved from the URL returned in the ‘link’ field, e.g. using the requests session available via the ses object property.
- Returns:
Response fields
- Return type:
dict
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
- profile()[source]¶
Request your Portokasse profile fields.
Example
>>> im.profile() {'ekp' : None, 'company' : 'ACME Inc', 'title' : 'Dr.', 'invoiceType' : 'PAPER', 'invoiceFrequency' : 'DECADE', 'mail' : 'myportokasse@example.org', 'firstname' : 'Erika', 'lastname' : 'Mustermann', 'street' : 'Heidestr.', 'houseNo' : '17', 'zip' : '51147', 'city' : 'Köln', 'country' : 'DEU', 'phone' : None, 'pobox' : None, 'poboxZip' : None, 'poboxCity' : None}
- Raises:
requests.exceptions.HTTPError – In case API endpoint responds with an error.
- inema.rest.calc_total(positions, products=default_products)[source]¶
Compute total of a positions based on a price dictionary.
- Parameters:
positions (list or dict or int) – Single product code or list of them, either as raw int or dictonary with
productCodekey.products (dict) – Dictionary that maps product code strings to dictionaries that contain the price at the cost_price key. See
data.productsfor the default prices.
- Returns:
The monetary sum of all positions in euro cent.
- Return type:
int
- inema.rest.check_health(api_base='https://api-eu.dhl.com/post/de/shipping/im/v1/')[source]¶
Check the availability and version of the Deutsche Post INTERNETMARKE REST API endpoint.
NB: doesn’t require a session login.
Example
>>> check_health() {'name' : 'pp-post-internetmarke', 'version' : 'v1.1.18', 'rev' : '35', 'env' : 'prod-eu'}
- inema.rest.compute_total(positions, im, pid2price=None)[source]¶
Compute total of a positions based on prices available online.
NB: This function requires and imports the pdfminer package.
- Parameters:
- Returns:
The monetary sum of all positions in euro cent.
- Return type:
int
- inema.rest.mk_addr(name, line, postcode, city, country='DEU', header=None, line2=None)[source]¶
Create address for postage label position object.
The result of this function can be used to supply the sender and receivers parameters of the
mk_pdf_pos()andmk_png_pos()functions.- Parameters:
name (str) – First name and last name, company name or something like that.
line (str) – Street and housenumber or postbox. NB: It’s required to be non-empty even though there are some real world postal addresses where only name, postcode and city are specified.
postcode (str) – Postleitzahl
country (str, default: 'DEU') – ISO 3166-1 alpha-3 three-letter country code. NB: when the DEU default is used the remote service leaves the country line blank.
header (str, optional) – Another line above the name field, e.g. put company here and department or person in the name field then.
line2 (str, optional) – Another line below the line field.
- Returns:
Dictionary that conforms to the inema Address schema.
- Return type:
dict
- inema.rest.mk_cancel_req(order_id=None, vouchers=None)[source]¶
Create postage cancellation request.
- Parameters:
order_id (str, optional) – Order ID used in a previous
Session.checkout_pdf()orSession.checkout_png()call.vouchers (list, optional) – List of voucher ID prevously returned by
Session.checkout_pdf()orSession.checkout_png()call or as read from the labels. Alternatively, list of pairs of voucher and tracking IDs.
- Returns:
Dictionary that conforms to the inema RetoureVouchersRequest schema.
- Return type:
dict
- Raises:
ValueError – In case a parameter violates the schema in an obvious way.
- inema.rest.mk_pdf_pos(product_code, column=1, row=1, page=1, layout='ADDRESS_ZONE', sender=None, receiver=None)[source]¶
Create a postage PDF label position.
The result (or a list of such results) can be used as a parameter of a
mk_png_req()call.Address information is optional, but if supplied both sender and receiver need to be supplied.
- Parameters:
product_code (int) – Product identifier from the Deutsche Post price list. See also
inema.data.productsor data/products.json or the Deutsche Post products webservice.column (int, default: 1) – when buying multiple positions and the format has multiple columns. one-based indexing
row (int, default: 1) – when buying multiple positions and the format has multiple rows. one-based indexing
page (int, default: 1) – when buying multiple positions and not all fit on one page. one-based indexing
layout ({ 'ADDRESS_ZONE', 'FRANKING_ZONE' }, default: 'ADDRESS_ZONE') – use the default when you supply sender and receiver and FRANKING_ZONE when not. The default layout also works without sender and receiver, but FRANKING_ZONE is less crammed then.
sender (dict) – Sender address information, created by
mk_addr().receiver (dict) – Receiver address information, created by
mk_addr().
- Returns:
Dictionary that conforms to the inema AppShoppingCartPDFPosition schema.
- Return type:
dict
- Raises:
ValueError – In case a parameter violates the schema in an obvious way.
KeyError – Invalid layout parameter.
- inema.rest.mk_pdf_preview_req(format_id, product_code, layout='ADDRESS_ZONE')[source]¶
Create PDF preview label request.
The result can be used as body parameter to a
Session.preview_pdf()call.Note that the preview doesn’t include any address information and thus is only useful for checking the placement of the QR code.
- Parameters:
format_id (int) – Page format ID. See also
inema.data.formatsor data/formats.jsonproduct_code (int) – Product identifier from the Deutsche Post price list. See also
inema.data.productsor data/products.json or the Deutsche Post products webservice.layout ({ 'ADDRESS_ZONE', 'FRANKING_ZONE' }, default: 'ADDRESS_ZONE') – use the default when you supply sender and receiver and FRANKING_ZONE when not. The default layout also works without sender and receiver, but FRANKING_ZONE is less crammed then.
- Returns:
Dictionary that conforms to the inema AppShoppingCartPreviewPDFRequest schema.
- Return type:
dict
- Raises:
KeyError – Invalid layout parameter.
- inema.rest.mk_pdf_req(order_id, format_id, positions, total, manifest=False)[source]¶
Create PDF checkout request.
The result can be used as body parameter of a
Session.checkout_pdf()call.- Parameters:
order_id (str) – Order ID returned by a previous
Session.create_order()call. Each request requires a fresh order ID.format_id (int) – Page format ID. See also
inema.data.formatsor data/formats.jsonpositions (list or object) – Value or list of values created by
mk_pdf_pos().total (integer) – Total monetary amount of all positions. See also
calc_total()andcompute_total()for a helper.manifest (bool, default: False) – Also request manifest link.
- Returns:
Dictionary that conforms to the inema AppShoppingCartPDFRequest schema.
- Return type:
dict
- Raises:
ValueError – In case a parameter violates the schema in an obvious way.
- inema.rest.mk_png_pos(product_code, layout='ADDRESS_ZONE', sender=None, receiver=None)[source]¶
Create a postage PNG label position.
The result (or a list of such results) can be used as a parameter of a
mk_png_req()call.Address information is optional, but if supplied both sender and receiver need to be supplied.
- Parameters:
product_code (int) – Product identifier from the Deutsche Post price list. See also
inema.data.productsor data/products.json or the Deutsche Post products webservice.layout ({ 'ADDRESS_ZONE', 'FRANKING_ZONE' }, default: 'ADDRESS_ZONE') – use the default when you supply sender and receiver and FRANKING_ZONE when not. The default layout also works without sender and receiver, but FRANKING_ZONE is less crammed then.
sender (dict) – Sender address information, created by
mk_addr().receiver (dict) – Receiver address information, created by
mk_addr().
- Returns:
Dictionary that conforms to the inema AppShoppingCartPosition schema.
- Return type:
dict
- Raises:
ValueError – In case a parameter violates the schema in an obvious way.
KeyError – Invalid layout parameter.
- inema.rest.mk_png_preview_req(product_code, layout='ADDRESS_ZONE')[source]¶
Create PNG preview label request.
The result can be used as body parameter to a
Session.preview_png()call.Note that the preview doesn’t include any address information and thus is only useful for checking the placement of the QR code.
- Parameters:
product_code (int) – Product identifier from the Deutsche Post price list. See also
inema.data.productsor data/products.json or the Deutsche Post products webservice.layout ({ 'ADDRESS_ZONE', 'FRANKING_ZONE' }, default: 'ADDRESS_ZONE') – use the default when you supply sender and receiver and FRANKING_ZONE when not. The default layout also works without sender and receiver, but FRANKING_ZONE is less crammed then.
- Returns:
Dictionary that conforms to the inema AppShoppingCartPreviewPDFRequest schema.
- Return type:
dict
- Raises:
KeyError – Invalid layout parameter.
- inema.rest.mk_png_req(order_id, positions, total, manifest=False)[source]¶
Create PNG checkout request.
The result can be used as body parameter of a
Session.checkout_png()call.- Parameters:
order_id (str) – Order ID returned by a previous
Session.create_order()call.positions (list or object) – Value or list of values created by
mk_pdf_pos().total (integer) – Total monetary amount of all positions. See also
calc_total()andcompute_total()for a helper.manifest (bool, default: False) – Also request manifest link.
- Returns:
Dictionary that conforms to the inema AppShoppingCartPNGRequest schema.
- Return type:
dict
- Raises:
ValueError – In case a parameter violates the schema in an obvious way.