Skip to main content

Connecting Magento 2 to Merit Aktiva: A Real Integration Architecture

Most Estonian webshops manage orders manually between their e-shop and accounting software. Here is the event-driven Node.js architecture that eliminates that entirely.

Back to Blog
Published · 25 Mar 2026
7 min read Updated 20 Jun 2026 Jaanek Liiskmaa
/ Integrations Node.js Magento
Connecting Magento 2 to Merit Aktiva: A Real Integration Architecture

The most common manual process in Estonian e-commerce is this: an order comes in on the webshop, and someone opens Merit Aktiva and creates an invoice. Then they check the warehouse, adjust the stock level, and maybe update the webshop.

This happens dozens or hundreds of times per day. Each step is a point of failure. Stock runs out unnoticed. Invoices are created late. Returns are forgotten.

The architecture to eliminate this is not complicated. It requires three components: a webhook receiver, a transformation layer, and an API client for Merit. Here is what that looks like in practice.

The Core Problem: Polling vs. Events

The first architectural decision is whether to poll or react.

Polling means your integration runs on a schedule - every 5 minutes, check for new orders in Magento, then process them. This is simple to implement and easy to debug. It also means you are always between 0 and 5 minutes late, your integration consumes compute whether or not anything has happened, and missed runs create backlogs.

Event-driven means Magento tells you when something happens. An order is placed → a webhook fires → your middleware processes it immediately. Latency is measured in milliseconds, not minutes.

For financial operations - invoices, inventory - you want events. A 5-minute-stale stock count is not acceptable when you have two customers trying to buy the last unit simultaneously.

The Architecture

Magento 2 (webshop)
    │
    │ Webhook: order.placed, shipment.created, refund.issued
    ▼
Node.js Middleware (your server)
    │
    ├── Validate event signature
    ├── Transform Magento order → Merit invoice format
    ├── Enqueue job (BullMQ + Redis)
    │
    ▼
Queue Worker
    │
    ├── POST /api/sales → Merit Aktiva API
    ├── On success → update Magento order status
    └── On failure → retry with exponential backoff, alert on dead letter

Each component has a single responsibility. The webhook receiver does nothing except validate and enqueue. The worker does nothing except call the Merit API and confirm success back to Magento. This separation means failures are isolated - a Merit API outage does not crash the webhook receiver.

Magento Webhook Configuration

Magento 2.4+ ships with a native webhook module. Configure it in app/etc/webhook.xml or through the Admin panel under System → Webhooks:

<webhook>
    <hook name="observer.sales_order_place_after">
        <url>https://your-middleware.example.com/webhooks/magento</url>
        <method>POST</method>
        <headers>
            <header name="X-Magento-Signature">{{HMAC_SHA256_SECRET}}</header>
        </headers>
        <fields>
            <field name="order_id">{{order.entity_id}}</field>
            <field name="increment_id">{{order.increment_id}}</field>
            <field name="customer_email">{{order.customer_email}}</field>
            <field name="grand_total">{{order.grand_total}}</field>
        </fields>
    </hook>
</webhook>

Always include an HMAC signature. Your middleware must reject any request that does not carry a valid signature - otherwise anyone who discovers the endpoint can inject fake orders into Merit.

The Transformation Layer

Magento and Merit speak different languages. Magento knows about order_items with sku, price, and qty_ordered. Merit knows about Lines with Item, Quantity, Price, and VATRate.

Write a pure transformation function - no side effects, no API calls:

function magentoOrderToMeritInvoice(order: MagentoOrder): MeritSalesDocument {
  return {
    Customer: {
      Name: order.billing_address.firstname + ' ' + order.billing_address.lastname,
      RegNo: order.billing_address.vat_id ?? '',
      Email: order.customer_email,
      Address: order.billing_address.street.join(', '),
      City: order.billing_address.city,
      CountryCode: order.billing_address.country_id,
    },
    DocDate: new Date().toISOString().split('T')[0],
    InvoiceNo: order.increment_id,
    Lines: order.items.map(item => ({
      Item: { Code: item.sku, Description: item.name },
      Quantity: item.qty_ordered,
      Price: item.price,
      VATRate: deriveVatRate(item),
      Unit: 'pc',
    })),
    TotalAmount: order.grand_total,
    Currency: order.order_currency_code,
  };
}

Keeping this as a pure function makes it trivially testable. You can write 50 unit tests covering VAT edge cases, partial refunds, and bundle products without touching the network.

Idempotency: The Part Most Integrations Skip

Merit's API does not prevent duplicate invoice creation. If your webhook fires twice (Magento retries on timeout), you will create two invoices for one order.

Solve this with an idempotency key stored in Redis before the API call:

async function processOrder(orderId: string) {
  const key = `merit:invoice:${orderId}`;
  
  const alreadyProcessed = await redis.get(key);
  if (alreadyProcessed) {
    logger.info({ orderId }, 'Order already invoiced, skipping');
    return;
  }

  const invoice = magentoOrderToMeritInvoice(await fetchMagentoOrder(orderId));
  await meritClient.createSalesDocument(invoice);
  
  // Set with TTL longer than your retry window
  await redis.set(key, '1', 'EX', 86400 * 7);
}

This makes the operation idempotent: processing the same order twice produces the same result as processing it once.

Stock Synchronization

The reverse flow - Merit updating Magento stock - is less time-critical. Inventory changes in the warehouse happen on shipment creation, which follows order processing. A 60-second polling interval from Merit's stock API to Magento's inventory API is acceptable here.

Merit does not push webhooks for stock changes. You need to poll their REST API:

GET https://aktiva.merit.ee/api/v1/getitems
Authorization: ApiId {YOUR_API_ID} + Signature {HMAC}

Diff the results against your last known state, then call the Magento Inventory API only for items that changed. Do not push 10,000 SKUs on every poll cycle.

Monitoring

An integration that runs silently for six months and then silently fails is worse than one that never worked, because you will not notice until a customer complains or a VAT audit surfaces missing invoices.

Add three monitoring signals from day one:

  1. Processing latency - time from webhook receipt to Merit confirmation. Alert if > 30 seconds.
  2. Dead letter queue depth - jobs that exhausted all retries. Alert if > 0.
  3. Daily reconciliation count - compare order count in Magento to invoice count in Merit for yesterday. Alert on mismatch.

A 15-minute cron running the reconciliation query costs almost nothing and catches the class of errors that idempotency and retry logic cannot - the silent failures where the API returned 200 but did not actually create the record.


This architecture runs in production on several Estonian e-commerce platforms. If you are connecting Magento to Merit Aktiva, Directo, Standard Books, or another local ERP, talk to us - we have done the edge cases already.

Frequently Asked Questions

Does Merit Aktiva have a REST API for integration?

Yes. Merit Aktiva provides a REST API at aktiva.merit.ee/api/v1/ covering sales documents (invoices, credit notes), customer records, item catalogue, and stock queries. Authentication uses HMAC-signed requests with an API ID and secret key assigned per integration. The API does not support outbound webhooks for stock changes - those must be retrieved via polling.

Should I use polling or webhooks to connect Magento to Merit Aktiva?

Use webhooks for the Magento → Merit direction (new orders, shipments, refunds). Magento 2.4+ has a native webhook module that fires events in real time with millisecond latency. Use polling for the Merit → Magento direction (stock levels), since Merit does not push events. A 60-second poll interval for inventory is acceptable for most stores.

How do I prevent duplicate invoices when integrating Magento with Merit Aktiva?

Merit's API does not prevent duplicate document creation if a request is submitted twice. Implement an idempotency layer in your middleware: before calling the Merit API, check Redis for a key containing the Magento order ID. If it exists, skip the call. If not, make the call and write the key with a TTL of at least 7 days. This handles Magento webhook retries without creating duplicate invoices.

What is the hardest part of a Magento-Merit Aktiva integration?

VAT rate derivation is the most common source of errors. Magento stores tax rules per product class and customer group; Merit requires an explicit VATRate on every line item. Edge cases - digital goods, B2B orders with reverse charge, bundled products with mixed tax classes - require a transformation layer tested against real order data before going to production.

How long does a Magento 2 to Merit Aktiva integration take to build?

A basic integration covering new order → invoice creation and stock synchronisation takes 4-8 weeks from requirements to production. The standard case - a physical product, an Estonian customer, a successful payment - takes an afternoon to implement. The remaining time covers edge cases: partial refunds, bundle products, failed payment retries, API outages, and the reconciliation monitoring needed to catch silent failures.

Need help with your architecture?

We help engineering teams build reliable, scalable systems.

Let's Talk