Integration guide: Connecting a third-party system to QHUB for receiving/sending Peppol documents via APEX
Intended for: Developers of external systems (ERP, accounting systems, portals) who want to connect to QCloud HUB and exchange Peppol documents (invoices, …) through the APEX Access Point.
1. How the integration works
Big picture
What QHUB does for you
- Routing — QHUB delivers messages from your system to APEX and back; you do not need to know where APEX runs.
- Retry & DLQ — on a transient outage QHUB retries the message; if all attempts fail, it goes to the Dead-Letter Queue.
- Audit — every message is recorded in the transaction table with status and timestamp.
What you need to do
| Direction | Your responsibility |
|---|---|
| Receiving (Peppol → your system) | Consume messages from your delivery queue; process the UBL XML; send a response back. |
| Sending (your system → Peppol) | Prepare the UBL XML document; send a synchronization message to the sync queue; track its status. |
2. Prerequisites & onboarding
2.1 What you need before starting
-
Peppol participant ID — your client must be registered in the Peppol network. The identifier has the format
{schemeId}:{participantId}, for example0245:2024000001.Scheme
0245= Slovak VAT ID in Peppol. If you do not know your Peppol ID, contact your Peppol Service Provider (SMP). -
ASOL contact — ask the ASOL team for:
- assignment of
SystemCodeof your system (e.g.MYERP), - assignment of
TenantId(UUID) of your QHUB tenant, - access to the QHUB Registration API (URL + API key).
- assignment of
-
RabbitMQ client — your system must be able to communicate with RabbitMQ (AMQP 0-9-1 protocol). We recommend:
- Java: Spring AMQP / RabbitMQ Java Client
- .NET: MassTransit / RabbitMQ.Client
- Python: pika
- Node.js: amqplib
2.2 Onboarding steps (overview)
1. Dohodnutie SystemCode + TenantId s ASOL
↓
2. Odoslanie popisu entity PeppolDocument do QHUB Registration API
↓
3. Administrátor ASOL vygeneruje jednorazový registračný kľúč
↓
4. Váš systém odošle kľúč na Registration API → získa RabbitMQ credentials
↓
5. Nakonfigurujete RabbitMQ v svojom systéme
↓
6. Otestujete end-to-end komunikáciu3. System registration in QHUB
Registration happens in two steps via HTTP REST API.
3.1 Submitting the entity schema
Before creating the registration you must inform QHUB which entities your system supports. For Peppol documents this is the entity PeppolDocument.
Endpoint:
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem/StoreSyncEntityItems Content-Type: application/json X-Api-Key: {váš API kľúč}
Request body:
{
"System": "MYERP",
"SyncEntityDatas": [
{
"EntityCode": "PeppolDocument",
"EntityType": 100,
"DefaultAllowed": true,
"Version": "1.0.0",
"SyncEntityDefinitionData": {
"SyncPropertiesData": [
{ "Name": "SenderParticipantId", "OriginalName": "senderParticipantId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "ReceiverParticipantId", "OriginalName": "receiverParticipantId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "DocumentTypeId", "OriginalName": "documentTypeId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "ProcessId", "OriginalName": "processId", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "UblDocument", "OriginalName": "ublDocument", "ProperyType": "String", "Required": true, "IsBusinessKey": false },
{ "Name": "As4MessageId", "OriginalName": "as4MessageId", "ProperyType": "String", "Required": false, "IsBusinessKey": true },
{ "Name": "ReceivedAt", "OriginalName": "receivedAt", "ProperyType": "DateTime", "Required": false, "IsBusinessKey": false }
],
"SyncForeignKeysData": []
}
}
]
}
EntityType: 100— the exact value is assigned by the ASOL team. Use the value you receive during onboarding. The number must be consistent across all systems.
Successful response: HTTP 200, body true
Repeat this call on every application start and on every schema change (and increment Version).
3.2 Validating the registration key
The ASOL administrator sends you via a secure channel (e.g. email, Jira) a one-time registration key (valid for 24 hours).
Endpoint:
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate Content-Type: application/json
Request body:
{
"ParticipantSystem": "MYERP",
"ParticipantTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"RegistrationKey": "REG-KEY-ODOSLANY-ADMINOM",
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
RoutingKey— for systems outside the ASOL Cloud (3ST) the format is{SystemCode}.{TenantId}. This key identifies your delivery queue.
Successful response (HTTP 200):
{
"ParticipantSystem": "MYERP",
"ParticipantTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"RabbitMqUrl": "amqps://rmq.qcloud.assecosoldevsk.com:5671",
"RabbitMqName": "myerp_tenant_abc123",
"RabbitMqPass": "••••••••••••",
"ErrorCode": null,
"Success": true,
"RegisteredEntities": [
{
"EntityType": 100,
"OwnerSystem": "APEX",
"OwnerTenantId": "...",
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"Operations": 1,
"Direction": 0,
"SyncMode": 0,
"AllowedFrom": "2026-04-16T00:00:00Z",
"AllowedTo": "2027-04-16T00:00:00Z",
"DeliverySystem": "MYERP",
"DeliveryTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}⚠️ Security:
RabbitMqPassis sensitive. Store it in Kubernetes Secret, HashiCorp Vault, or another secure store. Never commit it to source code or config files in your repository.
Error codes:
| Code | Description |
|---|---|
RegCodeNotFound | Key does not exist or multiple records were found |
RegCodeExpired | Key expired (valid for 24h) |
SystemMismatch | ParticipantSystem does not match the registration |
TenantMismatch | ParticipantTenantId does not match the registration |
HubInternalError | Internal QHUB error |
4. RabbitMQ connection setup
After successful registration you receive the RabbitMQ credentials. The connection uses AMQPS (TLS, port 5671).
4.1 Queues and exchanges
You must work with these queues and exchanges:
| Name | Type | Purpose |
|---|---|---|
clouderp-delivery-queue.MYERP.{TenantId} | Direct queue | Inbound of messages from QHUB to your system |
clouderp.sync | Fanout exchange | Sending of messages from your system to QHUB |
clouderp.sync.response | Fanout exchange | Sending a response about the processing result |
clouderp.delivery | Direct exchange | Source exchange from which QHUB populates the delivery queue |
4.2 Your RabbitMQ account permissions
Your account has these permissions:
| Operation | Resource |
|---|---|
| Configure + Read | clouderp-delivery-queue.MYERP.{TenantId} |
| Write | clouderp.sync (for sending documents to Peppol) |
| Write | clouderp.sync.response (for sending responses) |
4.3 Declaring your delivery queue
Your delivery queue must be declared by your system (not QHUB). Declare it before the first consume:
Meno: clouderp-delivery-queue.MYERP.{TenantId}
Durable: true
Auto-delete: false
Arguments:
x-dead-letter-exchange: clouderp.dlq (odporúčané)Binding to exchange:
Exchange: clouderp.delivery
Type: direct
Routing key: MYERP.{TenantId}5. Receiving documents from Peppol
When APEX receives a Peppol document addressed to your client, QHUB delivers it to your delivery queue as a SynchronizationMessage.
5.1 Receiving flow
Peppol odosielateľ
→ AS4 → APEX
→ QHUB (SynchronizationMessage, EntityType=PeppolDocument)
→ clouderp-delivery-queue.MYERP.{TenantId}
→ VÁŠ KONZUMER
→ spracujete UBL XML (importujete faktúru)
→ SynchronizationResponseMessage → clouderp.sync.response5.2 What you receive – SynchronizationMessage
The message arrives as a JSON object (see section 7):
{
"TransactionId": 123456,
"EventType": 1,
"EntityType": 100,
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerTenantId": "apex-tenant-uuid",
"OwnerSystem": "APEX",
"DeliverySystem": "MYERP",
"DeliveryTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"SyncKey": "550e8400-e29b-41d4-a716-446655440000",
"SchemaVersion": "1.0.0",
"Environment": "prod",
"DomainSyncVersion": 1,
"OccurredAt": "2026-04-16T10:30:00+02:00",
"Data": "{\"SenderParticipantId\":\"0245:2024000001\",\"ReceiverParticipantId\":\"0245:9999999999\",\"DocumentTypeId\":\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1\",\"ProcessId\":\"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\",\"UblDocument\":\"<?xml version=\\\"1.0\\\"...>\",\"As4MessageId\":\"uuid@peppol\",\"ReceivedAt\":\"2026-04-16T10:29:58+02:00\"}",
"IgnoredProperties": null
}The field Data is a JSON string (serialized once more) containing the entity PeppolDocument. It must be deserialized separately.
5.3 What to do after receiving
- Deserialize
DatatoPeppolDocument. - Check
DeliverySystemandDeliveryTenantId— they should match your system. - Import
UblDocument(UBL 2.1 XML) into your system. - Send a response
SynchronizationResponseMessageto the exchangeclouderp.sync.response.
5.4 Sending a response (required)
After each message is processed you must send a response back to QHUB. Without a response QHUB does not know whether the message was processed.
{
"TransactionId": 123456,
"Result": {
"Success": true,
"ErrorCode": null,
"ErrorParams": null
}
}On error:
{
"TransactionId": 123456,
"Result": {
"Success": false,
"ErrorCode": "ImportFailed",
"ErrorParams": ["Duplicitná faktúra INV-2026-001"]
}
}Important: Always send a response — even on error. If you do not respond, QHUB may redeliver the message later (retry).
6. Sending documents to Peppol
To send a document (e.g. an invoice) through Peppol, send a SynchronizationMessage with the entity PeppolDocument to the exchange clouderp.sync. QHUB delivers the message to APEX which performs the AS4 send.
6.1 Sending flow
VÁŠ SYSTÉM
→ SynchronizationMessage (EntityType=PeppolDocument)
→ clouderp.sync (fanout exchange)
→ QHUB Synchronization Service
→ APEX (clouderp-delivery-queue.APEX)
→ AS4 / HTTPS
→ Peppol príjemca
QHUB → SynchronizationResponseMessage → clouderp.sync.response
→ VÁŠ konzumer odpovede (voliteľné, ale odporúčané)6.2 What you must send
Send a SynchronizationMessage to the exchange clouderp.sync (without a routing key — it is a fanout):
{
"TransactionId": 0,
"EventType": 1,
"EntityType": 100,
"RoutingKey": "MYERP.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"OwnerSystem": "MYERP",
"DeliverySystem": null,
"DeliveryTenantId": null,
"SyncKey": "novy-uuid-pre-tuto-spravu",
"SchemaVersion": "1.0.0",
"Environment": "prod",
"DomainSyncVersion": 1,
"OccurredAt": "2026-04-16T10:30:00+02:00",
"Data": "{\"SenderParticipantId\":\"0245:2024000001\",\"ReceiverParticipantId\":\"0245:9999999999\",\"DocumentTypeId\":\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1\",\"ProcessId\":\"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\",\"UblDocument\":\"<?xml version=\\\"1.0\\\"...>\"}",
"IgnoredProperties": null
}
TransactionId: 0— when sending, set it to 0; QHUB assigns its own TransactionId on processing.
SyncKey— generate a new UUID for every message. It is the persistent identifier of the entity in QHUB synchronization.
DomainSyncVersion— start at 1. For every subsequent change of the same entity, increment by 1 (Lamport timestamp).
6.3 Tracking send result (optional)
QHUB responds with the AS4 send result via clouderp.sync.response. If you want to track delivery success/failure you must consume the responses.
Note: You receive a response from QHUB twice — once when APEX queues the document (optimistic success), and again after the actual AS4 send. This depends on how APEX implements the callback.
7. Message structures
All messages are JSON with PascalCase property names (compatible with .NET serialization).
7.1 SynchronizationMessage
Message used in both directions (sending and receiving).
| The field | Type | Description | Required |
|---|---|---|---|
TransactionId | long | Record identifier in the QHUB transaction table | áno |
EventType | int | Event type: 1=CREATE, 2=UPDATE, 4=DELETE | áno |
EntityType | int | Entity type: 100=PeppolDocument (TBD) | áno |
RoutingKey | string | Recipient routing key (MYERP.{TenantId}) | nie |
OwnerTenantId | string (UUID) | Source system tenant | áno |
OwnerSystem | string | Source system code (e.g. APEX, MYERP) | áno |
DeliverySystem | string | Target system code (populated by QHUB) | nie |
DeliveryTenantId | string (UUID) | Target system tenant (populated by QHUB) | nie |
SyncKey | string (UUID) | Unique persistent entity identifier | áno |
SchemaVersion | string | Entity schema version (SemVer, e.g. "1.0.0") | áno |
Environment | string | Environment: "prod", "test" | áno |
DomainSyncVersion | int | Entity change version (Lamport timestamp, ≥1) | áno |
OccurredAt | ISO 8601 datetime | Event creation time in the source system | áno |
Data | string (JSON) | Serialized PeppolDocument | áno |
IgnoredProperties | string[] | Properties the target should not ignore (populated by QHUB) | nie |
7.2 SynchronizationResponseMessage
Response your system sends after processing a received message.
| The field | Type | Description |
|---|---|---|
TransactionId | long | Copied from the received SynchronizationMessage |
Result.Success | boolean | true = processing completed successfully |
Result.ErrorCode | string | Error code (if Success=false) |
Result.ErrorParams | string[] | Additional error parameters |
8. Entity PeppolDocument
The entity transferred in the Data field of SynchronizationMessage.
8.1 Schema (version 1.0.0)
| The field | Type | Required | Business Key | Description |
|---|---|---|---|---|
SenderParticipantId | string | áno | nie | Sender Peppol ID. Format: {schemeId}:{id} |
ReceiverParticipantId | string | áno | nie | Recipient Peppol ID. Format: {schemeId}:{id} |
DocumentTypeId | string | áno | nie | Peppol Document Type URN (see Appendix A) |
ProcessId | string | áno | nie | Peppol Process URN (see Appendix A) |
UblDocument | string | áno | nie | Complete UBL 2.1 XML as a string |
As4MessageId | string | nie | áno | AS4 MessageId (present on inbound messages) |
ReceivedAt | ISO 8601 | nie | nie | Time received from the Peppol network (inbound) |
8.2 Entity example
{
"SenderParticipantId": "0245:2024000001",
"ReceiverParticipantId": "0245:9999999999",
"DocumentTypeId": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
"ProcessId": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
"UblDocument": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\">...</Invoice>",
"As4MessageId": "abc123@receiver.peppol.ap",
"ReceivedAt": "2026-04-16T10:29:58+02:00"
}8.3 Peppol Participant ID formats (most common)
| Country | Scheme | Example |
|---|---|---|
| Slovakia (VAT ID) | 0245 | 0245:2024000001 |
| Czech Republic | 0060 | 0060:12345678 |
| GLN | 0088 | 0088:1234567890123 |
9. Error codes and error handling
9.1 Registration errors
| Code | Cause | Resolution |
|---|---|---|
RegCodeNotFound | Invalid or non-existent registration key | Request a new key from the ASOL admin |
RegCodeExpired | Key expired (>24h) | Request a new key |
SystemMismatch | ParticipantSystem does not match | Check SystemCode |
TenantMismatch | ParticipantTenantId does not match | Check TenantId |
9.2 Synchronization errors (QHUB)
If QHUB cannot process your message, it sets the state to Failed and records:
| Code | Cause |
|---|---|
EventTypeMissing / EventTypeWrong | Invalid EventType |
EntityDataMissing | Missing field Data on CREATE/UPDATE |
TenantIdMissing | Missing OwnerTenantId |
SchemaVersionMissing | Missing SchemaVersion |
DomainSyncVersionWrong | DomainSyncVersion < 1 |
DeliverySystemWrong | DeliverySystem does not match the registration |
9.3 Your error codes (sent in the response)
V SynchronizationResponseMessage.Result.ErrorCode you may use any string code (we recommend English, no spaces). Examples:
| Code | When to use |
|---|---|
ImportFailed | Failed to import the document into your system |
DuplicateDocument | A document with this ID already exists |
ValidationFailed | UBL XML failed your business validation |
UnsupportedDocumentType | Your system does not support this document type |
UnexpectedError | Unexpected error |
9.4 Dead-Letter Queue
If you cannot process a message even after repeated attempts, QHUB moves it to the Dead-Letter Queue. The ASOL admin receives a notification. Contact ASOL support for resolution.
10. Implementation examples
10.1 Java (Spring AMQP)
Configuration
@Configuration public class QhubRabbitConfig { @Value("${qhub.rabbitmq.host}") private String host; @Value("${qhub.rabbitmq.port:5671}") private int port; @Value("${qhub.rabbitmq.username}") private String username; @Value("${qhub.rabbitmq.password}") private String password; @Value("${myapp.tenant-id}") private String tenantId; @Bean public ConnectionFactory qhubConnectionFactory() { CachingConnectionFactory cf = new CachingConnectionFactory(host, port); cf.setUsername(username); cf.setPassword(password); cf.setVirtualHost("/"); cf.getRabbitConnectionFactory().useSslProtocol(); // TLS povinné return cf; } @Bean public RabbitTemplate qhubRabbitTemplate(ConnectionFactory qhubConnectionFactory) { RabbitTemplate rt = new RabbitTemplate(qhubConnectionFactory); rt.setMessageConverter(new Jackson2JsonMessageConverter()); return rt; } @Bean public Queue myDeliveryQueue() { return QueueBuilder .durable("clouderp-delivery-queue.MYERP." + tenantId) .withArgument("x-dead-letter-exchange", "clouderp.dlq") .build(); } @Bean public DirectExchange deliveryExchange() { return new DirectExchange("clouderp.delivery", true, false); } @Bean public Binding myDeliveryBinding(Queue myDeliveryQueue, DirectExchange deliveryExchange) { return BindingBuilder.bind(myDeliveryQueue) .to(deliveryExchange) .with("MYERP." + tenantId); } }
Consumer (receiving)
@Slf4j @Component @RequiredArgsConstructor public class PeppolDocumentConsumer { private static final int PEPPOL_DOCUMENT_ENTITY_TYPE = 100; // dohodnuté s ASOL private final ObjectMapper objectMapper; private final RabbitTemplate qhubRabbitTemplate; private final MyInvoiceImportService importService; // vaša biznis logika @RabbitListener(queues = "#{myDeliveryQueue.name}") public void onMessage(SynchronizationMessage message) { if (message.getEntityType() != PEPPOL_DOCUMENT_ENTITY_TYPE) { return; // neznáma entita, ignorovať } SyncResult result; try { PeppolDocument doc = objectMapper.readValue( message.getData(), PeppolDocument.class); // Importujte UBL XML do vášho systému importService.importInvoice(doc.getUblDocument(), doc.getSenderParticipantId()); result = new SyncResult(true, null, null); } catch (DuplicateDocumentException e) { log.warn("Duplicate document, transactionId={}", message.getTransactionId()); result = new SyncResult(false, "DuplicateDocument", new String[]{e.getMessage()}); } catch (Exception e) { log.error("Failed to process PeppolDocument, transactionId={}", message.getTransactionId(), e); result = new SyncResult(false, "UnexpectedError", new String[]{e.getMessage()}); } // Vždy odošlite odpoveď sendResponse(message.getTransactionId(), result); } private void sendResponse(long transactionId, SyncResult result) { SynchronizationResponseMessage response = new SynchronizationResponseMessage(transactionId, result); qhubRabbitTemplate.convertAndSend("clouderp.sync.response", "", response); } }
Sending a document
@Slf4j @Component @RequiredArgsConstructor public class PeppolDocumentSender { private static final int PEPPOL_DOCUMENT_ENTITY_TYPE = 100; private static final int EVENT_TYPE_CREATE = 1; private static final String SCHEMA_VERSION = "1.0.0"; private final RabbitTemplate qhubRabbitTemplate; private final ObjectMapper objectMapper; @Value("${myapp.system-code:MYERP}") private String systemCode; @Value("${myapp.tenant-id}") private String tenantId; /** * Odošle UBL XML dokument do QHUB → APEX → Peppol. */ public void send(String senderParticipantId, String receiverParticipantId, String documentTypeId, String processId, String ublXml) throws Exception { PeppolDocument doc = new PeppolDocument(); doc.setSenderParticipantId(senderParticipantId); doc.setReceiverParticipantId(receiverParticipantId); doc.setDocumentTypeId(documentTypeId); doc.setProcessId(processId); doc.setUblDocument(ublXml); SynchronizationMessage msg = new SynchronizationMessage(); msg.setEventType(EVENT_TYPE_CREATE); msg.setEntityType(PEPPOL_DOCUMENT_ENTITY_TYPE); msg.setRoutingKey(systemCode + "." + tenantId); msg.setOwnerSystem(systemCode); msg.setOwnerTenantId(tenantId); msg.setSyncKey(UUID.randomUUID().toString()); msg.setSchemaVersion(SCHEMA_VERSION); msg.setEnvironment("prod"); msg.setDomainSyncVersion(1); msg.setOccurredAt(OffsetDateTime.now()); msg.setData(objectMapper.writeValueAsString(doc)); qhubRabbitTemplate.convertAndSend("clouderp.sync", "", msg); log.info("Document sent to QHUB: sender={}, receiver={}", senderParticipantId, receiverParticipantId); } }
10.2 .NET (MassTransit / RabbitMQ.Client)
MassTransit configuration
services.AddMassTransit(config => { config.AddConsumer<PeppolDocumentConsumer>(); config.UsingRabbitMq((ctx, cfg) => { cfg.Host(configuration["Qhub:RabbitMq:Host"], "/", h => { h.Username(configuration["Qhub:RabbitMq:Username"]); h.Password(configuration["Qhub:RabbitMq:Password"]); h.UseSsl(s => s.Protocol = SslProtocols.Tls12); }); // Konfigurácia odosielania (sync queue) cfg.Message<SynchronizationMessage>(p => p.SetEntityName("clouderp.sync")); // Konfigurácia odposielania odpovede cfg.Message<SynchronizationResponseMessage>(p => p.SetEntityName("clouderp.sync.response")); // Delivery queue s binding na exchange cfg.ReceiveEndpoint( $"clouderp-delivery-queue.MYERP.{tenantId}", e => { e.ConfigureConsumeTopology = false; e.UseMessageRetry(r => r.Immediate(1)); e.Bind("clouderp.delivery", x => { x.RoutingKey = $"MYERP.{tenantId}"; x.ExchangeType = ExchangeType.Direct; }); e.ConfigureConsumer<PeppolDocumentConsumer>(ctx); }); }); });
Consumer
public class PeppolDocumentConsumer : IConsumer<SynchronizationMessage> { private const int PeppolDocumentEntityType = 100; private readonly IInvoiceImportService _importService; private readonly IPublishEndpoint _publishEndpoint; public PeppolDocumentConsumer(IInvoiceImportService importService, IPublishEndpoint publishEndpoint) { _importService = importService; _publishEndpoint = publishEndpoint; } public async Task Consume(ConsumeContext<SynchronizationMessage> context) { var message = context.Message; if (message.EntityType != PeppolDocumentEntityType) return; SyncResult result; try { var doc = JsonSerializer.Deserialize<PeppolDocument>(message.Data!); await _importService.ImportAsync(doc.UblDocument, doc.SenderParticipantId); result = SyncResult.Ok(); } catch (DuplicateDocumentException ex) { result = SyncResult.Fail("DuplicateDocument", new[] { ex.Message }); } catch (Exception ex) { result = SyncResult.Fail("UnexpectedError", new[] { ex.Message }); } await _publishEndpoint.Publish( new SynchronizationResponseMessage(message.TransactionId, result)); } }
Sending a document
public class PeppolDocumentSender { private const int PeppolDocumentEntityType = 100; private const string SchemaVersion = "1.0.0"; private readonly IPublishEndpoint _bus; private readonly string _systemCode; private readonly string _tenantId; public PeppolDocumentSender(IPublishEndpoint bus, IConfiguration config) { _bus = bus; _systemCode = config["MyApp:SystemCode"]!; _tenantId = config["MyApp:TenantId"]!; } public async Task SendAsync(string senderParticipantId, string receiverParticipantId, string documentTypeId, string processId, string ublXml, CancellationToken ct = default) { var doc = new PeppolDocument { SenderParticipantId = senderParticipantId, ReceiverParticipantId = receiverParticipantId, DocumentTypeId = documentTypeId, ProcessId = processId, UblDocument = ublXml }; var message = new SynchronizationMessage { EventType = 1, // CREATE EntityType = PeppolDocumentEntityType, RoutingKey = $"{_systemCode}.{_tenantId}", OwnerSystem = _systemCode, OwnerTenantId = _tenantId, SyncKey = Guid.NewGuid().ToString(), SchemaVersion = SchemaVersion, Environment = "prod", DomainSyncVersion = 1, OccurredAt = DateTimeOffset.UtcNow, Data = JsonSerializer.Serialize(doc) }; await _bus.Publish(message, ct); } }
10.3 Python (pika)
import json import ssl import uuid from datetime import datetime, timezone import pika QHUB_HOST = "rmq.qcloud.assecosoldevsk.com" QHUB_PORT = 5671 QHUB_USER = "myerp_tenant_abc123" QHUB_PASS = "••••••••" MY_SYSTEM_CODE = "MYERP" MY_TENANT_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ENTITY_TYPE = 100 # PeppolDocument DELIVERY_QUEUE = f"clouderp-delivery-queue.{MY_SYSTEM_CODE}.{MY_TENANT_ID}" SYNC_EXCHANGE = "clouderp.sync" RESPONSE_EXCHANGE = "clouderp.sync.response" def connect(): ssl_ctx = ssl.create_default_context() credentials = pika.PlainCredentials(QHUB_USER, QHUB_PASS) params = pika.ConnectionParameters( host=QHUB_HOST, port=QHUB_PORT, credentials=credentials, ssl_options=pika.SSLOptions(ssl_ctx, QHUB_HOST) ) return pika.BlockingConnection(params) # ── Prijímanie dokumentov ────────────────────────────────────────────────── def on_message(channel, method, properties, body): msg = json.loads(body) if msg.get("EntityType") != ENTITY_TYPE: channel.basic_ack(delivery_tag=method.delivery_tag) return transaction_id = msg["TransactionId"] try: doc = json.loads(msg["Data"]) ubl_xml = doc["UblDocument"] # --- vaša biznis logika --- import_invoice(ubl_xml, doc["SenderParticipantId"]) result = {"Success": True, "ErrorCode": None, "ErrorParams": None} except Exception as e: result = {"Success": False, "ErrorCode": "UnexpectedError", "ErrorParams": [str(e)]} response = {"TransactionId": transaction_id, "Result": result} channel.basic_publish( exchange=RESPONSE_EXCHANGE, routing_key="", body=json.dumps(response), properties=pika.BasicProperties(content_type="application/json") ) channel.basic_ack(delivery_tag=method.delivery_tag) def start_consuming(): conn = connect() channel = conn.channel() channel.queue_declare(queue=DELIVERY_QUEUE, durable=True, arguments={"x-dead-letter-exchange": "clouderp.dlq"}) channel.queue_bind(queue=DELIVERY_QUEUE, exchange="clouderp.delivery", routing_key=f"{MY_SYSTEM_CODE}.{MY_TENANT_ID}") channel.basic_consume(queue=DELIVERY_QUEUE, on_message_callback=on_message) channel.start_consuming() # ── Odosielanie dokumentov ────────────────────────────────────────────────── def send_document(sender_id, receiver_id, document_type_id, process_id, ubl_xml): doc = { "SenderParticipantId": sender_id, "ReceiverParticipantId": receiver_id, "DocumentTypeId": document_type_id, "ProcessId": process_id, "UblDocument": ubl_xml, } message = { "TransactionId": 0, "EventType": 1, "EntityType": ENTITY_TYPE, "RoutingKey": f"{MY_SYSTEM_CODE}.{MY_TENANT_ID}", "OwnerSystem": MY_SYSTEM_CODE, "OwnerTenantId": MY_TENANT_ID, "SyncKey": str(uuid.uuid4()), "SchemaVersion": "1.0.0", "Environment": "prod", "DomainSyncVersion": 1, "OccurredAt": datetime.now(timezone.utc).isoformat(), "Data": json.dumps(doc), } conn = connect() channel = conn.channel() channel.basic_publish( exchange=SYNC_EXCHANGE, routing_key="", body=json.dumps(message), properties=pika.BasicProperties(content_type="application/json", delivery_mode=2) # persistent ) conn.close() def import_invoice(ubl_xml: str, sender_id: str): # Vaša implementácia importu pass
11. FAQ
How do I know a new invoice has arrived?
Attach a consumer to the delivery queue clouderp-delivery-queue.MYERP.{TenantId}. A message with EntityType=100 and OwnerSystem=APEX is a document from the Peppol network.
Can I send document types other than invoices?
Yes. UblDocument can contain any Peppol BIS 3.0 document (credit note, order, …). Just set the correct DocumentTypeId and ProcessId (see Appendix A).
What if sending through Peppol fails?
APEX automatically retries the send (exponential backoff, max 5 attempts). You receive the result via SynchronizationResponseMessage with Success=false and the appropriate ErrorCode.
Is the UBL XML validated?
Yes, APEX validates every document before sending (XSD schema, EN 16931 Schematron, Peppol BIS 3.0 rules). If the document is invalid you receive Success=false.
What is the Peppol scheme code for Slovakia?
Slovak Company ID in the Peppol network uses scheme 0245, so the ID has the format 0245:{DIČ}, e.g. 0245:2024000001.
How do I verify that a recipient is registered in the Peppol network?
APEX verifies this automatically (SMP lookup) before sending. If the recipient is not registered you get an error PARTICIPANT_NOT_FOUND.
Do I have to preserve message order?
For standalone documents (each invoice is a new message) order is not critical. If you send multiple versions of the same document (UPDATE operations), use DomainSyncVersion to resolve conflicts.
Appendix A: Peppol Document Type identifiers
Most common document types
| Document type | DocumentTypeId | ProcessId |
|---|---|---|
| Invoice | urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 |
| Credit Note | urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 |
| MLR (Message Level Response) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:2017:pracc:t111:ver1.0::2.1 | urn:fdc:peppol.eu:2017:pracc:messagelevels:01:1.0 |
| IMR (Invoice Message Response) | urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:2017:poacc:billing:08:1.0::2.1 | urn:fdc:peppol.eu:2017:poacc:billing:08:1.0 |
Appendix B: Integration checklist
- Received
SystemCodeandTenantIdfrom ASOL - Received URL and API key for the QHUB Registration API
- Submitted entity schema
PeppolDocumentto the Registration API - Received one-time registration key from ASOL admin
- Called endpoint
POST /api/v1/Registration/Validate - RabbitMQ credentials stored securely
- Delivery queue declared with binding
- Consumer (receiving) implemented
- Consumer sends a
SynchronizationResponseMessageafter each processing - Sending implemented (if needed)
- Inbound flow tested: test AS4 message → your application
- Outbound flow tested: your application → APEX → Peppol test environment
Appendix C: Contacts & support
| Request type | Contact |
|---|---|
| Onboarding (SystemCode, TenantId, API key) | ASOL integration team |
| QHUB technical support | ASOL DevOps / Cloud team |
| Peppol technical support (certificates, SMP registration) | ASOL Peppol team / your SMP provider |
| Bug report | Customer Portal |