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]