Integration guide: Connecting a third-party system to QHUB for receiving/sending Peppol documents via APEX

Version: 1.0  •  Date: 2026-04-16
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

☁ ASOL CLOUD 🖥️ Your system ERP · Accounting system · Portal message consumer & producer 🐇 RabbitMQ AMQPS · async messages QCloud HUB Synchronization Service Routing · Retry & DLQ · Audit · Transactions HUB APEX Peppol Access Point (Java) SMP Lookup · AS4 · UBL validation AS4 AS4 / HTTPS 🌐 Peppol Network European eInvoicing network response / delivery

What QHUB does for you

What you need to do

DirectionYour 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

  1. Peppol participant ID — your client must be registered in the Peppol network. The identifier has the format {schemeId}:{participantId}, for example 0245:2024000001.

    Scheme 0245 = Slovak VAT ID in Peppol. If you do not know your Peppol ID, contact your Peppol Service Provider (SMP).

  2. ASOL contact — ask the ASOL team for:
    • assignment of SystemCode of your system (e.g. MYERP),
    • assignment of TenantId (UUID) of your QHUB tenant,
    • access to the QHUB Registration API (URL + API key).
  3. 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)

steps
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áciu

3. 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:

http
POST {QHUB_REGISTRATION_URL}/api/v1/SyncEntityCatalogItem/StoreSyncEntityItems
Content-Type: application/json
X-Api-Key: {váš API kľúč}

Request body:

json
{
  "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:

http
POST {QHUB_REGISTRATION_URL}/api/v1/Registration/Validate
Content-Type: application/json

Request body:

json
{
  "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):

json
{
  "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: RabbitMqPass is 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:

CodeDescription
RegCodeNotFoundKey does not exist or multiple records were found
RegCodeExpiredKey expired (valid for 24h)
SystemMismatchParticipantSystem does not match the registration
TenantMismatchParticipantTenantId does not match the registration
HubInternalErrorInternal 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:

NameTypePurpose
clouderp-delivery-queue.MYERP.{TenantId}Direct queueInbound of messages from QHUB to your system
clouderp.syncFanout exchangeSending of messages from your system to QHUB
clouderp.sync.responseFanout exchangeSending a response about the processing result
clouderp.deliveryDirect exchangeSource exchange from which QHUB populates the delivery queue

4.2 Your RabbitMQ account permissions

Your account has these permissions:

OperationResource
Configure + Readclouderp-delivery-queue.MYERP.{TenantId}
Writeclouderp.sync (for sending documents to Peppol)
Writeclouderp.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:

config
Meno:       clouderp-delivery-queue.MYERP.{TenantId}
Durable:    true
Auto-delete: false
Arguments:
  x-dead-letter-exchange: clouderp.dlq   (odporúčané)

Binding to exchange:

config
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

diagram
Peppol odosielateľ
    → AS4 → APEX
        → QHUB (SynchronizationMessage, EntityType=PeppolDocument)
            → clouderp-delivery-queue.MYERP.{TenantId}
                → VÁŠ KONZUMER
                    → spracujete UBL XML (importujete faktúru)
                        → SynchronizationResponseMessage → clouderp.sync.response

5.2 What you receive – SynchronizationMessage

The message arrives as a JSON object (see section 7):

json
{
  "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

  1. Deserialize Data to PeppolDocument.
  2. Check DeliverySystem and DeliveryTenantId — they should match your system.
  3. Import UblDocument (UBL 2.1 XML) into your system.
  4. Send a response SynchronizationResponseMessage to the exchange clouderp.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.

json
{
  "TransactionId": 123456,
  "Result": {
    "Success": true,
    "ErrorCode": null,
    "ErrorParams": null
  }
}

On error:

json
{
  "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

diagram
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):

json
{
  "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 fieldTypeDescriptionRequired
TransactionIdlongRecord identifier in the QHUB transaction tableáno
EventTypeintEvent type: 1=CREATE, 2=UPDATE, 4=DELETEáno
EntityTypeintEntity type: 100=PeppolDocument (TBD)áno
RoutingKeystringRecipient routing key (MYERP.{TenantId})nie
OwnerTenantIdstring (UUID)Source system tenantáno
OwnerSystemstringSource system code (e.g. APEX, MYERP)áno
DeliverySystemstringTarget system code (populated by QHUB)nie
DeliveryTenantIdstring (UUID)Target system tenant (populated by QHUB)nie
SyncKeystring (UUID)Unique persistent entity identifieráno
SchemaVersionstringEntity schema version (SemVer, e.g. "1.0.0")áno
EnvironmentstringEnvironment: "prod", "test"áno
DomainSyncVersionintEntity change version (Lamport timestamp, ≥1)áno
OccurredAtISO 8601 datetimeEvent creation time in the source systemáno
Datastring (JSON)Serialized PeppolDocumentáno
IgnoredPropertiesstring[]Properties the target should not ignore (populated by QHUB)nie

7.2 SynchronizationResponseMessage

Response your system sends after processing a received message.

The fieldTypeDescription
TransactionIdlongCopied from the received SynchronizationMessage
Result.Successbooleantrue = processing completed successfully
Result.ErrorCodestringError code (if Success=false)
Result.ErrorParamsstring[]Additional error parameters

8. Entity PeppolDocument

The entity transferred in the Data field of SynchronizationMessage.

8.1 Schema (version 1.0.0)

The fieldTypeRequiredBusiness KeyDescription
SenderParticipantIdstringánonieSender Peppol ID. Format: {schemeId}:{id}
ReceiverParticipantIdstringánonieRecipient Peppol ID. Format: {schemeId}:{id}
DocumentTypeIdstringánoniePeppol Document Type URN (see Appendix A)
ProcessIdstringánoniePeppol Process URN (see Appendix A)
UblDocumentstringánonieComplete UBL 2.1 XML as a string
As4MessageIdstringnieánoAS4 MessageId (present on inbound messages)
ReceivedAtISO 8601nienieTime received from the Peppol network (inbound)

8.2 Entity example

json
{
  "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)

CountrySchemeExample
Slovakia (VAT ID)02450245:2024000001
Czech Republic00600060:12345678
GLN00880088:1234567890123

9. Error codes and error handling

9.1 Registration errors

CodeCauseResolution
RegCodeNotFoundInvalid or non-existent registration keyRequest a new key from the ASOL admin
RegCodeExpiredKey expired (>24h)Request a new key
SystemMismatchParticipantSystem does not matchCheck SystemCode
TenantMismatchParticipantTenantId does not matchCheck TenantId

9.2 Synchronization errors (QHUB)

If QHUB cannot process your message, it sets the state to Failed and records:

CodeCause
EventTypeMissing / EventTypeWrongInvalid EventType
EntityDataMissingMissing field Data on CREATE/UPDATE
TenantIdMissingMissing OwnerTenantId
SchemaVersionMissingMissing SchemaVersion
DomainSyncVersionWrongDomainSyncVersion < 1
DeliverySystemWrongDeliverySystem 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:

CodeWhen to use
ImportFailedFailed to import the document into your system
DuplicateDocumentA document with this ID already exists
ValidationFailedUBL XML failed your business validation
UnsupportedDocumentTypeYour system does not support this document type
UnexpectedErrorUnexpected 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

java
@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)

java
@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

java
@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

csharp
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

csharp
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

csharp
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)

python
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 typeDocumentTypeIdProcessId
Invoiceurn: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.1urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
Credit Noteurn: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.1urn: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.1urn: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.1urn:fdc:peppol.eu:2017:poacc:billing:08:1.0

Appendix B: Integration checklist


Appendix C: Contacts & support

Request typeContact
Onboarding (SystemCode, TenantId, API key)ASOL integration team
QHUB technical supportASOL DevOps / Cloud team
Peppol technical support (certificates, SMP registration)ASOL Peppol team / your SMP provider
Bug reportCustomer Portal