Integration API

REST do tworzenia i obsługi zamówień w Ecommigo. Integracja sklepu, ERP albo wtyczki WooCommerce czyta i zapisuje zamówienia na koncie powiązanym z kluczem.

Base URL prod https://integration.ecommigo.com/v1
dev https://integration-dev.ecommigo.com/v1

Autoryzacja

Każde żądanie wymaga nagłówka x-api-key. Klucz jest w panelu Ecommigo, w konfiguracji integracji.

x-api-key: <twój klucz>

Brak klucza → 401. Nieprawidłowy klucz → 403. API nie przyjmuje accountId w body — konto wynika z klucza.

Zamówienia

GET, PATCH i DELETE widzą wszystkie zamówienia konta (także te z panelu). POST tworzy nowe. Identyfikator w ścieżce to id, numer sklepu (externalNumber, np. SHOP-1001) albo zapisany header.documentNumber (często 5756 — API nie składa ZK/2026/0001).

GET/orders

Lista zamówień konta, od najnowszych. Wąska strona: header/kontrahent/płatność/pozycje (key, sku, quantity, price/unitPrice), produkt bez gallery, externalDocuments: []. Bez positionKey, totalValue, vatRate, methodTitle, paidAmount. Pełny kontrakt jest na GET /orders/{id}. Zamiast skip użyj cursor.

QueryOpis
limitDomyślnie 50, max 200. Powyżej → 400 limit_exceeded (bez cichego capu)
cursorNastępna strona (wartość nextCursor)
skipOffset (wolniejszy niż cursor)
scopemine — tylko zamówienia utworzone przez tę integrację

Odpowiedź 200

{
  "items": [
    {
      "_id": "6a6da0cd0ceabb38f6cade11",
      "id": "6a6da0cd0ceabb38f6cade11",
      "header": {
        "symbol": "ZK",
        "externalNumber": "SHOP-1001",
        "documentNumber": "5756",
        "status": "new"
      },
      "positions": [],
      "externalDocuments": []
    }
  ],
  "nextCursor": null
}

Przykład

curl -H "x-api-key: $API_KEY" \
  "https://integration-dev.ecommigo.com/v1/orders?limit=10"
GET/orders/{id}

Jedno zamówienie. {id} to identyfikator API, SHOP-1001 albo zapisany header.documentNumber (np. 5756). Odpowiedź to dokument EasyHub: pełny contractor/payment/positions (m.in. positionKey, unitPrice, totalValue, vatRate, methodTitle, paidAmount gdy są w Mongo), wąski positions.product (_id, id, name, sku, ean, gallery), externalDocuments jako metadane plików (nie zagnieżdżone FA/WZ). Lista GET /orders zostaje wąska.

Odpowiedź 200

{
  "_id": "6adb5f26221a11a212042ad3",
  "id": "6adb5f26221a11a212042ad3",
  "header": {
    "symbol": "ZK",
    "externalNumber": "SHOP-1001",
    "documentNumber": "5756",
    "status": "new",
    "customStatus": null
  },
  "contractor": {
    "name": "Acme Sp. z o.o.",
    "email": "ops@acme.test",
    "companyName": "Acme Sp. z o.o.",
    "nip": "5252341234"
  },
  "payment": {
    "method": "bacs",
    "methodTitle": "Przelew tradycyjny",
    "paid": false,
    "paidAmount": 0
  },
  "positions": [
    {
      "key": "a1b2c3d4",
      "positionKey": "a1b2c3d4",
      "name": "Sneakersy Beżowe 39",
      "sku": "SHBZ18835239",
      "quantity": 1,
      "unitPrice": 19.99,
      "price": 19.99,
      "totalValue": 19.99,
      "vatRate": 23,
      "product": {
        "_id": "6a6b4776a2ca9b8864f7645d",
        "id": "6a6b4776a2ca9b8864f7645d",
        "name": "Sneakersy Beżowe 39",
        "sku": "SHBZ18835239",
        "ean": "5901234567890",
        "gallery": [{ "url": "https://cdn.example/shbz.jpg" }]
      }
    }
  ],
  "customFields": [],
  "externalDocuments": [
    {
      "_id": "6adb5f26aaaa111122223333",
      "id": "6adb5f26aaaa111122223333",
      "documentNumber": "FA/2026/0012",
      "filename": "fa.pdf",
      "s3Url": "https://ecommigo-dev-private.s3.eu-central-1.amazonaws.com/fa.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900",
      "downloadUrl": "https://ecommigo-dev-private.s3.eu-central-1.amazonaws.com/fa.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=900",
      "documentType": "invoice",
      "createdAt": "2026-01-02T00:00:00.000Z"
    }
  ],
  "logs": [],
  "createdAt": "2026-10-23T13:20:38.240Z",
  "updatedAt": "2026-10-23T13:20:38.240Z"
}
POST/orders

Tworzy zamówienie i nadaje documentNumber. Wymagane externalNumber. Ten sam numer przy ponownym POST zwraca istniejące zamówienie (200) zamiast tworzyć drugie.

Każda pozycja musi wskazywać produkt z katalogu: productId albo sku (ew. ean). Nieznany produkt → 400 unknown_product. Odpowiedź zwraca wąski positions.product (_id, id, name, sku, ean, gallery) oraz externalDocuments jako pliki ExternalDocument (bez zagnieżdżonego document).

Request

{
  "externalNumber": "SHOP-1001",
  "status": "new",
  "contractor": {
    "name": "Acme Sp. z o.o.",
    "email": "ops@acme.test"
  },
  "payment": { "method": "bacs", "paid": false },
  "positions": [
    { "sku": "SHBZ18835239", "quantity": 2, "price": 19.99 }
  ],
  "customFields": [
    { "key": "source", "value": "shop" }
  ]
}

Odpowiedź 201

{
  "_id": "6adb5f26…",
  "id": "6adb5f26…",
  "header": { "symbol": "ZK", "externalNumber": "SHOP-1001", "documentNumber": "5756", "status": "new" },
  "positions": [
    {
      "sku": "SHBZ18835239",
      "name": "Sneakersy Beżowe 39",
      "quantity": 2,
      "price": 19.99,
      "product": {
        "_id": "6a6b4776…",
        "id": "6a6b4776…",
        "sku": "SHBZ18835239",
        "name": "Sneakersy Beżowe 39",
        "ean": "5901234567890"
      }
    }
  ],
  "externalDocuments": []
}
PATCH/orders/{id}

Częściowa aktualizacja (status, płatność, pozycje, kontrahent, pola własne). Numer dokumentu i właściciel zamówienia się nie zmieniają.

Request

{
  "status": "paid",
  "payment": { "method": "bacs", "paid": true }
}
DELETE/orders/{id}

Usuwa zamówienie.

Odpowiedź 200

{ "deleted": true, "id": "SHOP-1001" }

Słowniki

Wartości do formularzy i mapowania. Wszystkie zwracają { "items": [ … ] }. Opcjonalne limit: bez parametru = wszystkie wiersze; powyżej 200 → 400 limit_exceeded (ten sam kształt co produkty, bez cichego capu).

GET/payment-methods

Metody płatności konta. Pole payment.method na zamówieniu to key.

{
  "items": [
    {
      "id": "6a6b03e4…",
      "key": "bacs",
      "name": "Przelew tradycyjny",
      "isActive": true,
      "codAvailable": false
    }
  ]
}
GET/shipping-methods

Metody wysyłki. key wstawiasz jako metodę dostawy na zamówieniu.

{
  "items": [
    { "id": "…", "key": "inpost", "name": "InPost", "isActive": true, "iconUrl": null }
  ]
}
GET/order-statuses

Statusy z Mongo statuses (konto + wiersze bez accountId). Pusta lista = brak statusów własnych na koncie; zamówienia i tak używają header.status (new, paid).

GET/custom-fields

Pola własne. Na zamówieniu: customFields: [{ "key", "value" }].

Produkty i stany magazynowe

Kolekcja Mongo products na koncie z klucza. Stan jest na dokumencie produktu: stock, manageStock, alertStock (liczba), lowStock (liczba — próg jak alertStock). GET dodaje wyliczone isLowStock (stock <= lowStock). Boolean w lowStock przy PATCH → 400. Osobnej kolekcji stanów nie ma. Lista jest wąska i stronicowana kursorem.

GET/products

Wąski katalog. Query: limit (domyślnie 50, max 200 — powyżej 400 limit_exceeded), cursor, sku, ean. Bez gallery i opisów. Lista pomija produkty usunięte (__deleted__).

{
  "items": [
    {
      "id": "65f0c1a2…",
      "name": "Widget",
      "sku": "SKU-1",
      "ean": "590123",
      "stock": 12,
      "manageStock": true,
      "alertStock": 2,
      "lowStock": 2,
      "isLowStock": false
    }
  ],
  "nextCursor": "MjAyNi0wMS0wM3…"
}
GET/products/{id}

Jeden produkt po _id, sku albo ean. Dodatkowo pola EasyHub: gallery ([{ src, alt, _id }]; CloudFront src bez podpisu, leftover S3 nadal podpisywany), category (tablica), purchasePrice / price (tablica { value }), productCode, type, unit, packageDetails, catalogNumber, status / isActive, parent, defaultVariantId. Gdy type: variant, dochodzi variants[] (variantPosition).

POST/products

Tworzy dokument jak EasyHub UI create: category: [], productCode, manageStock + stock: 0 na produkcie, flagi, packageDetails. Wymagane sku i name (także na children). Puste name → 400 bad_request. Ten sam SKU → 409 conflict. gallery / images zapisuje EasyHub [{ src, alt, _id }] (nigdy stringi). Tylko nasz CloudFront / public bucket — obce URL (bocco.com, data:) → 400 external_gallery_url. Upload pliku na create: { filename, contentType, data }. Rodzic + warianty: type: variant i children / variants (dzieci variantPosition, parent, prefix productCode) albo osobny POST z parentId. Opcjonalne stock / initialStock zapisuje magazineitems (in + confirmed). warehouseId z requestu jest honorowane; bez niego — WarehouseSettings isDefault. Opcjonalne locationId. GET stock nakłada sumę z magazynu. Puste catalogNumber / manufacturerCode nie są zapisywane. Stan: PATCH /stock, nie PUT /products-stocks.

{
  "sku": "SHIRT",
  "name": "Shirt",
  "type": "variant",
  "gallery": [{ "src": "https://d111111abcdef8.cloudfront.net/products/{accountId}/{productId}/shirt.jpg", "alt": "" }],
  "stock": 15,
  "warehouseId": "65f0c1a2b3d4e5f67890abcd",
  "children": [
    { "sku": "RED", "name": "Shirt Red" },
    { "sku": "BLUE", "name": "Shirt Blue" }
  ]
}
POST/products/sku

Następny unikalny SKU jak EasyHub: seria numberingsettings documentType: SKU ($inc nextNumber, ten sam format co ZK). Pomija wartości już na products.sku. Nie tworzy produktu. Brak serii → auto-insert {NUMBER} (nextNumber: 0, pierwsze 0001). Istniejąca seria (np. SKUv1-{NUMBER}) zostaje. Opcjonalnie parentId (wariant, prefix rodzica w productCode) albo productId (zapis na istniejący wiersz, gdy sku jest puste).

{
  "sku": "0008",
  "productCode": {
    "parentPrefix": "",
    "localValue": "0008",
    "fullValue": "0008",
    "tokens": ["0008"]
  }
}
POST/products/purchase-code

Następny unikalny kod zakupowy. Seria documentType: PURCHASE_CODE. Unikalność względem products.code / purchaseCode / additionalCodes. Pole na produkcie to code. Brak serii → auto-insert {NUMBER} i pierwsze 0001. Opcjonalne productId zapisuje kod, gdy pole jest puste.

{
  "purchaseCode": "0009",
  "code": "0009"
}
POST/products/{id}/images

Upload zdjęcia produktu na ten sam publiczny S3 + CloudFront co EasyHub. Zapisuje gallery[] jako { src, alt, _id } (CloudFront). JSON z data (base64) albo multipart/form-data / surowy image/* → Lambda PutObject. Bez bajtów: presigned PUT (uploadUrl, 900s). Obcy host → 400 external_gallery_url. Brak bucketa → 503 media_unavailable.

{
  "filename": "lamp.jpg",
  "contentType": "image/jpeg",
  "alt": ""
}
PATCH/products/{id}

Aktualizacja pól katalogowych po _id / sku / ean. Nie zmienia sku ani stanu. Może dopisać children / variants (promuje rodzica do type: variant). Stan: PATCH albo PUT /stock.

GET/stock

Ta sama kolekcja, tylko pola do synchronizacji stanów. Query jak produkty: limit (domyślnie 50, max 200 — powyżej 400 limit_exceeded), cursor, sku, ean. Gdy produkt ma tablicę warehouses, odpowiedź zwraca { id, stock }.

GET/stock/{id}

Stan jednego produktu po _id / sku / ean.

PATCH/stock

Hurtowa aktualizacja stanu (max 200). Każda zmiana to nowy ruch magazineitems (in/out na abs(diff), confirmed). warehouseId z pozycji jest honorowane; bez niego — WarehouseSettings isDefault. Z podanym magazynem delta liczy się wobec stanu w tym magazynie. Opcjonalne locationId. EasyHub UI odejmuje tylko out i pomija wiersze bez magazynu. Po zapisie: product.resources.update i DEL Redis product:magazine:{accountId}:{productId}. To jest właściwy endpoint — nie ma PUT /products-stocks. Działa też PUT /stock. Identyfikator: productId, sku albo ean. Nieznane SKU idą do errors[]. Same nieznane → 200 { "updated": 0, "items": [], "errors": [ … ] }, nie top-level 400.

{
  "items": [
    { "sku": "SKU-1", "stock": 20, "warehouseId": "65f0c1a2b3d4e5f67890abcd" },
    { "sku": "SKU-3", "stock": 11, "warehouses": [{ "id": "WH-A", "stock": 7 }] }
  ]
}

Odpowiedź 200

{
  "updated": 1,
  "items": [{ "id": "…", "sku": "SKU-1", "stock": 20 }],
  "errors": [{ "sku": "MISSING", "code": "unknown_product", "message": "Unknown product (MISSING)" }]
}

Magazyny i lokalizacje

Mongo warehousesettings (EasyHub WarehouseSettings, accountId z klucza). Lista magazynów jest wąska: tylko id, type, name. Lokalizacje (tysiące na magazyn) są osobną stroną — nigdy zagnieżdżone na GET /warehouses.

GET/warehouses

Strona magazynów. Query jak zamówienia: limit (domyślnie 50, max 200 — powyżej 400 limit_exceeded), cursor.

{
  "items": [
    { "id": "65f0c1a2…", "type": "warehouse", "name": "Main" }
  ],
  "nextCursor": "MjAyNi0wMS0wM3…"
}
GET/warehouses/{id}/locations

Lokalizacje jednego magazynu. Ten sam wynik: GET /locations?warehouseId=. Kolekcja Mongo locations albo zagnieżdżone WarehouseSettings.locations (agregacja, stronicowana). Brak warehouseId na /locations → 400. Nieznany magazyn → 404.

{
  "items": [
    { "id": "65f0c1a2…", "type": "bin", "name": "A-1", "warehouseId": "65f0c1a2…" }
  ],
  "nextCursor": null
}

Posty sprzedażowe

Ten sam dokument co EasyHub / EasyLive (posts). Alias /sales-posts. Wideo transkoduje istniejący post-media-worker (CloudFront media-dev.jest.live, klucz public/post-media/…).

POST/posts

Wymagane title (albo name) i media. Puste title → 400. Status domyślnie draft (published / scheduled / archived). scheduled wymaga scheduledAt. productIds muszą istnieć na koncie. Media tylko nasz CloudFront — obce URL → 400 external_media_url. Duże pliki: bez data, presigned PUT albo S3 multipart (≥ 8 MB / multipart: true), potem complete. Kafka posts.created gdy nic nie jest w uploading.

{
  "title": "Live drop",
  "description": "Tonight",
  "productIds": ["SKU-1"],
  "media": [
    { "filename": "clip.mp4", "contentType": "video/mp4", "size": 250000000 }
  ]
}

multipart/form-data: pola title, description, status, productIds + plik file / video / image (małe bajty, Lambda PutObject).

GET/posts/{id}

Jeden post po _id. media.src to źródło na CDN. hls / mp4 / poster pojawiają się po transkodzie workera.

POST/posts/{id}/media/{mediaId}/complete

Po klienckim PUT (albo częściach S3 multipart z { parts: [{ partNumber, etag }] }). Wideo → processing i posts.created jeśli to ostatni upload.

Kody błędów

Błędy aplikacji:

{ "error": { "code": "not_found", "message": "Order not found" } }
HTTPcodeZnaczenie
400bad_requestBrak externalNumber / sku / name / title / media / warehouseId, nieznany magazyn / lokalizacja produktu, albo niepoprawny JSON
400external_media_urlURL mediów posta poza naszym CloudFront
400unknown_productProdukt spoza katalogu konta
400limit_exceededKażda lista: limit powyżej 200 (bez cichego capu)
409conflictSKU już istnieje
401—Brak nagłówka x-api-key
403—Nieprawidłowy klucz
404not_foundNie ma takiego zamówienia, produktu, magazynu albo posta na koncie
503numbering_unavailableKonto nie ma serii ZK w numberingsettings (POST /orders). SKU / PURCHASE_CODE tworzą serię {NUMBER} same