Erpy for Craft CMS

Connectors

Each ERP is a separate free add-on plugin that registers a connector with Erpy. Install the one for your ERP and it appears in the connector list.

ERPPackagePlugin handle
Microsoft Dynamics 365 Business Centraljustinholtweb/craft-erpy-businesscentralerpy-businesscentral
Oracle NetSuitejustinholtweb/craft-erpy-netsuiteerpy-netsuite
Acumaticajustinholtweb/craft-erpy-acumaticaerpy-acumatica
SAP Business Onejustinholtweb/craft-erpy-sapb1erpy-sapb1
Sage Intacct, Sage 200, Sage X3, Sage Accountingjustinholtweb/craft-erpy-sageerpy-sage
Odoojustinholtweb/craft-erpy-odooerpy-odoo
Exact Onlinejustinholtweb/craft-erpy-exactonlineerpy-exactonline
AFAS Profitjustinholtweb/craft-erpy-afaserpy-afas
Visma.net ERPjustinholtweb/craft-erpy-vismaerpy-visma
MYOB Acumatica, MYOB Exojustinholtweb/craft-erpy-myoberpy-myob
Unit4 ERPjustinholtweb/craft-erpy-unit4erpy-unit4
Priorityjustinholtweb/craft-erpy-priorityerpy-priority

Each ERP's page covers what its connector syncs, the credentials it asks for and where to find them, and the vendor's own traps. The add-ons have no documentation of their own; it all lives here.

One package may register several connectors — Sage registers four, MYOB two.

A Mock ERP connector ships with Erpy itself, makes no network calls, and invents deterministic data. See Installation.

Why the add-ons are free

A connector's whole job is to turn one page of one vendor's payloads into canonical documents. It does not queue, retry, page, dedupe, map, log, schedule or write to Commerce — because Erpy does, once, for all of them. There is not enough left in a connector to charge for, and pricing them separately would mean a merchant on an unusual ERP paying more for less.

It is the same arrangement Imager-X uses for its transformers, for the same reason: the interesting work is not in any one integration.

What a connector declares

Every connector publishes a capability list, and Erpy's screens are built from it. That is why you are never offered a direction your ERP cannot do.

public static function capabilities(): Capabilities
{
    return Capabilities::make()
        ->supports(Entity::PRODUCT, Direction::PULL, delta: true, pageSize: 200)
        ->supports(Entity::ORDER, Direction::PUSH);
}

Whatever it declares, it must implement. Erpy ships a conformance suite — a TCK — that drives every installed connector through the same checks against a recorded transport. It is what catches a connector advertising a flow it never wrote, a delta sync sending no filter, paging that repeats a cursor, credentials that never reach the request, a refusal marked retryable so the queue hammers the ERP, and a credential echoed back to the merchant.

Writing one

A connector is one class. Register it and it appears everywhere — the connection form, the capability matrix, the mapping screen, the conformance suite.

Event::on(
    Connectors::class,
    Connectors::EVENT_REGISTER_CONNECTORS,
    static function(RegisterComponentTypesEvent $event) {
        $event->types[] = MyErpConnector::class;
    },
);
class MyErpConnector extends Connector
{
    public static function handle(): string { return 'my-erp'; }
    public static function displayName(): string { return 'My ERP'; }
    public static function vendor(): string { return 'Acme'; }

    public static function capabilities(): Capabilities
    {
        return Capabilities::make()
            ->supports(Entity::PRODUCT, Direction::PULL, delta: true, pageSize: 200)
            ->supports(Entity::ORDER, Direction::PUSH);
    }

    public static function settingsFields(): array
    {
        return [
            Field::url('baseUrl', 'API URL', ['required' => true]),
            Field::secret('apiKey', 'API key', ['required' => true]),
        ];
    }

    protected function buildAuth(): ?AuthInterface
    {
        return new ApiKeyAuth();
    }

    protected function fetchProducts(FetchCriteria $criteria): Page
    {
        $response = $this->transport()->get('items', ['limit' => $criteria->limit]);

        return new Page(
            array_map(fn(array $row) => new ErpProduct([...]), $response->at('items', [])),
            $response->at('nextCursor'),
        );
    }

    protected function pushOrder(ErpOrder $document, ?string $remoteId = null): PushResult
    {
        // …
    }
}

Erpy provides the auth strategies most ERPs need — API key, HTTP basic, session, OAuth 1.0a two-legged, OAuth 2 client credentials and authorization code — so a connector picks one rather than writing a token refresh.

The transport redacts secrets

An ERP will echo your credential back inside its own error message. Erpy's transport strips secrets out of non-2xx bodies before a connector — or a merchant's health screen — ever sees them. Success bodies are left alone, because connectors have to parse them.

When the connector's field name is wrong

Several of these ERPs are configured per customer: AFAS, Priority, Unit4 and a published Business Central page can all name the same concept differently on two tenants. A connector cannot be right about that in advance.

That is what the mapping overlay is for. A mapping rule whose target is a canonical field overwrites what the connector read, before the document reaches Commerce — so a wrong field name is an afternoon's fix rather than a bug report. See Field mapping.

More in Accounting & ERP

Pairs well with Erpy

Commerce orders, landed in the books your client already uses. One plugin per accounting package, built around the tax rules of the country it's sold in, plus Erpy for when the client runs a full ERP.