How to use services correctly in libcanard

I am a bit stuck in understanding a proper way of using the services.

So if I am correct, I need both publisher and subscriber to perform the service call as there is no callback in v1.0.

At the beginning, I assume we would need a subscriber:

(void) canardRxSubscribe(&ins,                        // Subscribe to an arbitrary service response.
                         123,                         // The Service-ID to subscribe to.
                         1024,                        // The maximum payload size (max DSDL object size).

Then, if I want to call a service, I just need to create a transfer object and push it (publish):

const CanardTransfer transfer = {
    .timestamp_usec = transmission_deadline,      // Zero if transmission deadline is not limited.
    .priority       = CanardPriorityNominal,
    .transfer_kind  = CanardTransferKindMessage,
    .port_id        = 123,                       // This is the service-ID.
    .remote_node_id = CANARD_NODE_ID_UNSET,   
    .transfer_id    = my_message_transfer_id,
    .payload_size   = 47,
    .payload        = "\x2D\x00" "Sancho, it strikes me thou art in great fear.",

int32_t result = canardTxPush(&ins, &transfer);

This will send a message to the remote_id, the remote_id will read the service id and transfer id and do some tasks and send back the payload.
And after publishing this, I would then need to accept a frame:

CanardTransfer transfer;
const int8_t result = canardRxAccept(&ins,
                                     &received_frame,            // The CAN frame received from the bus.
                                     redundant_interface_index,  // If the transport is not redundant, use 0.

Is that a correct flow? I got this understanding from reading the header file

Not quite. See, we have two kinds of communication: messages and services. Messages can be published and subscribed to; services can be invoked (or called) and provided (or served). You can’t “publish” a service call or “subscribe” to a service, it doesn’t make sense. In Libcanard, there is a bit of an implementation-specific terminology mishap: it uses “subscribe” in the sense of “tell the library that the application wants to receive this kind of transfers”; the mishap is because I couldn’t find a better word to use. Maybe I should have used “listen” instead, like canardRxListen? I wonder if we should change it.

So if you want to provide a service, you tell the library to receive service request transfers, like this:

// Configure the library to listen for register access service requests.
CanardRxSubscription srv_register_access;
(void) canardRxSubscribe(&canard,
                         1024U,  // Larger buffers are OK.

Then you handle the calls like this:

// Process received frames, if any.
CanardFrame rxf;
uint8_t     buffer[64];
while (socketcanPop(sock, &rxf, sizeof(buffer), buffer, 1000) > 0)  // Error handling not implemented
    CanardTransfer transfer;
    if (canardRxAccept(&canard, &rxf, 0, &transfer))
        if ((transfer.transfer_kind == CanardTransferKindRequest) &&
            (transfer.port_id == RegisterAccessServiceID))
            handleRegisterAccess(&canard, &transfer);
        free((void*) transfer.payload);
static void handleRegisterAccess(CanardInstance* const canard, const CanardTransfer* const request_transfer)
    <... business logic not shown ...>
    // Send the response back. Make sure to re-use the same priority and transfer-ID.
    CanardTransfer response_transfer = *request_transfer;
    response_transfer.transfer_kind  = CanardTransferKindResponse;
    response_transfer.payload_size   = response_size;
    response_transfer.payload        = response;
    (void) canardTxPush(canard, &response_transfer);

When invoking a service, you inverse the logic: listen for service response transfers and emit service request transfers. When receiving the responses, match them by their transfer-ID value.

The examples above have been copy-pasted from this demo: Automatic configuration of port identifiers