Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Ibanity API (part 2) : documents

This section explores the inbox and outbox management subset of the Ibanity API, which is used internally by the lino_xl.lib.peppol plugin. As a Lino application developer you won’t need to know these details if you just use the plugin.

About this document

This page contains code snippets (lines starting with >>>), which are being tested during our development workflow. The following snippet initializes the demo project used throughout this page.

>>> from lino_book.projects.cosi1.startup import *

The code snippets in this document are tested only if you have Ibanity credentials installed:

>>> if not dd.plugins.peppol.credentials:
...     pytest.skip('this doctest requires Ibanity credentials')

This doctest communicates with the Ibanity environment.

>>> ar = rt.login("robin")
>>> ses = dd.plugins.peppol.get_ibanity_session(ar)
>>> from lino_book.projects.noi1e.settings.data import suppliers
>>> DEMO_SUPPLIER_ID = dd.plugins.peppol.supplier_id
>>> DEMO_SUPPLIER_ID == suppliers[0].supplier_id
True
>>> OTHER_SUPPLIER_ID = suppliers[1].supplier_id
>>> from lino_xl.lib.peppol.utils import supplier_attrs, res2str, EndpointID

Outbound documents

To send an outbound document, we need its XML file. Let’s take one that has been generated by Lino.

>>> qs = trading.VatProductInvoice.objects.filter(journal__ref="SLS")
>>> qs = qs.filter(partner__send_peppol=True)
>>> qs = qs.filter(partner__name__startswith="Number")
>>> obj = qs.order_by("accounting_period__year", "number").last()
>>> obj
VatProductInvoice #292 ('SLS 46/2024')
>>> # obj.clear_cache()  # remove pdf file from previous runs
>>> xmlfile = obj.make_xml_file(ar)
>>> xmlfile.path.exists()
True
>>> res = ses.create_outbound_document(DEMO_SUPPLIER_ID, xmlfile.path)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '...',
                         'status': 'created',
                         'technicalStatus': 'created'},
          'id': '...',
          'relationships': {'supplier': {'data': {'id': '...',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> assert res['data']['attributes']['status'] == "created"
>>> assert res['data']['relationships']['supplier']['data']['id'] == DEMO_SUPPLIER_ID
>>> doc_id = res['data']['id']

Each submitted outbound document gets tested by the Ibanity server for potential syntax errors in the XML, and we can consult the results:

>>> res = ses.get_outbound_document(DEMO_SUPPLIER_ID, doc_id)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '...',
                         'status': '...',
                         'technicalStatus': '...'},
          'id': '...',
          'relationships': {'supplier': {'data': {'id': '...',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> assert res['data']['attributes']['status'] in {'created', 'sending'}

The status returned in above snippet is sometimes ‘created’ and sometimes ‘sending’. We need to give the server some time to run their tests. So let’s sleep for a couple of seconds and then repeat the same request.

>>> import time
>>> time.sleep(5)
>>> res = ses.get_outbound_document(DEMO_SUPPLIER_ID, doc_id)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '...',
                         'status': 'sent',
                         'technicalStatus': 'sent',
                         'transmissionId': '...'},
          'id': '...',
          'relationships': {'supplier': {'data': {'id': '...',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> assert res['data']['attributes']['status'] in {'sent'}

Above snippet works in an integration environment only because simulate_endpoints is True.

Otherwise it would return an error because our fictive customer has a random VAT number, which is not registered on any SMP. Here is how such an error would look like:

{'data': {'attributes': {'createdAt': '...',
                         'errors': [{'code': 'error-customer-not-registered',
                                     'detail': 'Identifier '
                                               "'iso6523-actorid-upis::9925:be1773515336' "
                                               'is not registered in SML.'}],
                         'status': 'send-error',
                         'technicalStatus': 'send-error'},
          'id': '...',
          'relationships': {'supplier': {'data': {'id': '...',
                                                  'type': 'supplier'}}},
          'type': 'peppolInvoice'}}
>>> obj.partner
Partner #95 ('Number Three')
>>> obj.partner.vat_id
'BE 0345.678.997'

It is allowed to send the invoice again, and Ibanity will assign it another doc_id. Each such sending of an invoice triggers its own test workflow.

>>> res = ses.create_outbound_document(DEMO_SUPPLIER_ID, xmlfile.path)
>>> assert res['data']['id'] != doc_id

We can also ask for a list of all sendings we’ve made in a given period of time.

>>> from datetime import datetime, timedelta
>>> from pytz import utc
>>> start_time = datetime.now(utc) - timedelta(hours=1)
>>> res = ses.list_outbound_documents(DEMO_SUPPLIER_ID, start_time)
>>> pprint(res)
{'data': [{'attributes': {'createdAt': '2025-06-16T01:11:56.449Z',
                          'status': 'sending',
                          'technicalStatus': 'sending'},
           'id': 'b43f7340-5c7d-4fa6-867e-c85bbf788c84',
           'relationships': {'supplier': {'data': {'id': '997dc48c-b953-4588-81c0-761871e37e42',
                                                   'type': 'supplier'}}},
           'type': 'peppolInvoice'},
          {'attributes': {'createdAt': '2025-06-16T01:11:53.384Z',
                          'errors': [{'code': 'error-customer-not-registered',
                                      'detail': 'Identifier '
                                                "'iso6523-actorid-upis::9925:be0506780656' "
                                                'is not registered in SML.'}],
                          'status': 'send-error',
                          'technicalStatus': 'send-error'},
           'id': '9f77ed9b-7952-4dc7-aa4b-6bf0b8478809',
           'relationships': {'supplier': {'data': {'id': '997dc48c-b953-4588-81c0-761871e37e42',
                                                   'type': 'supplier'}}},
           'type': 'peppolInvoice'},
          ...
          {'attributes': {'createdAt': '2025-06-16T00:24:14.521Z',
                          'errors': [{'code': 'invalid-schematron',
                                      'detail': 'Document MUST not contain '
                                                'empty elements. '
                                                '(/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName)\n'
                                                'Document MUST not contain '
                                                'empty elements. '
                                                '(/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:AdditionalStreetName)\n'
                                                'Document MUST not contain '
                                                'empty elements. '
                                                '(/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:PostalZone)'}],
                          'status': 'invalid',
                          'technicalStatus': 'invalid'},
           'id': 'bd4f131f-abdf-4abc-a695-000ef1f95ce2',
           'relationships': {'supplier': {'data': {'id': '997dc48c-b953-4588-81c0-761871e37e42',
                                                   'type': 'supplier'}}},
           'type': 'peppolInvoice'}],
 'meta': {'paging': {'number': 0, 'size': 2000, 'total': 1}}}

Above snippet is not tested because the results defer depending on how many times this document has been tested during the last hour.

Inbound documents

>>> # pytest.skip("This part fails and I don't yet understand why.")
>>> time.sleep(5)
>>> res = ses.list_inbound_documents(OTHER_SUPPLIER_ID)
>>> sorted(res.keys())
['data', 'meta']
>>> inbound_docs = res['data']
>>> pprint(inbound_docs)
[{'attributes': {'createdAt': '...',
                 'transmissionId': '...'},
  'id': '...',
  'relationships': {'supplier': {'data': {'id': '...',
                                          'type': 'supplier'}}},
  'type': 'peppolInboundDocument'}]
>>> doc_info = res['data'][0]
>>> doc_id = doc_info['id']
>>> res = ses.get_inbound_document_xml(doc_id)
>>> type(res)
<class 'str'>
>>> len(res)
21640
>>> print(res[:120])
<?xml version="1.0" encoding="UTF-8"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac=

We can also ask for the JSON object describing the inbound document:

>>> res = ses.get_inbound_document_json(doc_id)
>>> pprint(res)
{'data': {'attributes': {'createdAt': '...',
                         'transmissionId': '...'},
          'id': '...',
          'relationships': {'supplier': {'data': {'id': '...',
                                                  'type': 'supplier'}}},
          'type': 'peppolInboundDocument'}}

This returns the same information as what was returned by list_inbound_documents():

>>> assert res['data'] == inbound_docs[0]