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: object

Internetmarke 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() or checkout_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.formats

or 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() call

  • transaction_id (int, optional) – retoureTransactionId field previously returned by a cancel() call

  • start (str, optional) – start of query period in ISO 8601 datetime format with T delimiter and without timezone

  • end (str, optional) – end of query period in ISO 8601 datetime format with T delimiter 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() or checkout_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 productCode key.

  • products (dict) – Dictionary that maps product code strings to dictionaries that contain the price at the cost_price key. See data.products for 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:
  • positions (list or dict or int) – Single product code or list of them, either as raw int or dictonary with productCode key.

  • im (Session) – Session object for querying the prices.

  • pid2price (dict, optional) – Dictionary to cache product prices for the current and future invocations.

Returns:

The monetary sum of all positions in euro cent.

Return type:

int

inema.rest.formats2csv(formats, filename)[source]
inema.rest.formats2json(formats, filename)[source]
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() and mk_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:
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.products or 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.formats or data/formats.json

  • product_code (int) – Product identifier from the Deutsche Post price list. See also inema.data.products or 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.formats or data/formats.json

  • 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() and compute_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.products or 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.products or 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() and compute_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.