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.