# Conekto Marketing — documentația pentru dezvoltatori

API: https://marketing.conekto.app/api/v1. Toate cererile au antetul `Authorization: Bearer <cheie>`.

## Primii pași

API-ul primește datele magazinului tău (clienți, produse, comenzi, evenimente) din orice platformă. Shopify și WooCommerce sunt conectate automat; restul folosesc API-ul de mai jos.

- Creează o cheie în Setări → General → Chei API. Pentru probe, alege „Creează cheie de test”.
- Verifică cheia cu GET /ping.
- Trimite întâi categoriile și produsele, apoi clienții, apoi comenzile (comenzile se leagă de clienți și produse prin external_id).
- Pune scriptul pe site pentru evenimentele de navigare (vizualizări, coș, căutări).

**Verifică cheia**

```bash
curl https://marketing.conekto.app/api/v1/ping \
  -H "Authorization: Bearer ck_live_…"
```

**Răspuns**

```json
{
  "ok": true,
  "test": false,
  "shop": {
    "name": "Magazinul Meu",
    "domain": "magazin.ro",
    "platform": "custom",
    "public_key": "pk_…"
  }
}
```

## Autentificare și limite

Fiecare cerere are antetul Authorization cu cheia magazinului. Cheia identifică magazinul, deci nu trimiți niciodată un id de magazin.

| Regulă | Valoare |
| --- | --- |
| Antet | `Authorization: Bearer ck_live_…` (sau `ck_test_…`) |
| Format | JSON, `Content-Type: application/json`, UTF-8 |
| Limită de cereri | 600 pe minut pe cheie (în medie 10 pe secundă) |
| Mărimea unui lot | între 1 și 500 de obiecte într-o cerere |
| Sume | număr sau text cu punct zecimal: `349`, `"349.00"`; se rotunjesc la 2 zecimale |
| Date | ISO 8601 cu fus orar: `2026-11-03T20:15:00+02:00` |

> Păstrează cheia pe server. Nu o pune în codul din browser: scriptul de pe site folosește cheia publică (pk_…), care poate doar trimite evenimente.

## Răspunsuri și erori

Un lot nu se oprește la primul obiect greșit: obiectele bune se salvează, iar cele greșite apar în errors, cu poziția lor în listă.

**Răspuns la un lot (200)**

```json
{
  "received": 2,
  "created": 1,
  "updated": 0,
  "errors": [
    {
      "index": 1,
      "message": "email: Contactul are nevoie de email sau telefon."
    }
  ]
}
```

| Cod | Când | Ce faci |
| --- | --- | --- |
| 200 | Lotul a fost procesat (poate avea erori pe obiecte) | Verifică `errors`; retrimite doar obiectele de la acele poziții, corectate |
| 400 | Body-ul nu are forma `{ <listă>: [...] }` sau are peste 500 de obiecte | Corectează forma cererii |
| 401 | Cheia lipsește, e greșită sau a fost retrasă | Creează o cheie nouă din Setări |
| 403 | Magazinul nu mai e activ | Scrie-ne la suport |
| 405 | Altă metodă decât POST (sau GET la /ping) | Folosește metoda corectă |
| 429 | Peste 600 de cereri pe minut | Așteaptă câte secunde spune antetul `Retry-After`, apoi reia |
| 5xx | Eroare la noi | Reîncearcă după câteva secunde, cu pauze tot mai mari |

Retrimiterea aceluiași obiect e sigură: totul se salvează după external_id, deci a doua oară obiectul doar se actualizează (updated crește, nu created).

## Contacte

POST /contacts creează sau actualizează clienți după external_id (id-ul din platforma ta). Fiecare contact are nevoie de email sau telefon.

**POST /contacts**

```json
{
  "contacts": [
    {
      "external_id": "1042",
      "email": "ana.pop@exemplu.ro",
      "phone": "0722123456",
      "first_name": "Ana",
      "last_name": "Pop",
      "city": "Cluj-Napoca",
      "county": "Cluj",
      "country": "RO",
      "email_consent": {
        "state": "subscribed",
        "updated_at": "2026-10-02T09:14:00+03:00"
      },
      "sms_consent": {
        "state": "not_subscribed"
      },
      "orders_count": 3,
      "total_spent": "742.50",
      "last_order_at": "2026-10-28T18:40:00+02:00",
      "created_at": "2025-03-11T10:00:00+02:00"
    }
  ]
}
```

| Câmp | Tip | Descriere |
| --- | --- | --- |
| external_id | text, obligatoriu | Id-ul clientului în platforma ta |
| email / phone | text | Cel puțin unul. Telefonul se normalizează la formatul internațional (+40…) |
| first_name, last_name, city, county | text | Folosite în personalizare și segmente |
| country | 2 litere | `RO`, `MD`… |
| email_consent / sms_consent | obiect | `state`: subscribed, not_subscribed, unsubscribed sau pending; `updated_at` opțional |
| orders_count, total_spent, last_order_at | număr / sumă / dată | Istoricul, dacă nu trimiți comenzile; altfel se calculează din comenzi |
| created_at | dată | Când și-a făcut clientul cont |

> Acordul de marketing se schimbă doar dacă trimiți email_consent sau sms_consent; fără ele, contactul își păstrează starea. Un „subscribed” nu anulează o dezabonare făcută la noi decât dacă are în updated_at o dată mai nouă decât dezabonarea.

## Categorii

POST /categories. Trimite-le înaintea produselor; ierarhia se face cu parent_external_id.

**POST /categories**

```json
{
  "categories": [
    {
      "external_id": "12",
      "name": "Încălțăminte",
      "url": "https://magazin.ro/incaltaminte"
    },
    {
      "external_id": "31",
      "name": "Sneakers",
      "url": "https://magazin.ro/incaltaminte/sneakers",
      "parent_external_id": "12"
    }
  ]
}
```

## Produse

POST /products. Produsele apar în recomandări, în blocurile de produse din emailuri și în fluxurile de stoc și preț.

**POST /products**

```json
{
  "products": [
    {
      "external_id": "4421",
      "title": "Sneakers Urban Alb",
      "url": "https://magazin.ro/sneakers-urban-alb",
      "image_url": "https://magazin.ro/img/4421.jpg",
      "brand": "Urban",
      "categories": [
        "31"
      ],
      "tags": [
        "nou",
        "vara"
      ],
      "status": "active",
      "variants": [
        {
          "external_id": "4421-40",
          "sku": "URB-40",
          "title": "40",
          "price": "349.00",
          "compare_at_price": "399.00",
          "in_stock": true,
          "stock_qty": 6
        },
        {
          "external_id": "4421-41",
          "sku": "URB-41",
          "title": "41",
          "price": "349.00",
          "in_stock": false,
          "stock_qty": 0
        }
      ]
    },
    {
      "external_id": "88",
      "title": "Cutie cadou",
      "price": 19.9,
      "in_stock": true
    }
  ]
}
```

- `categories` are external_id-urile categoriilor trimise la /categories.
- `variants`: până la 250. Un produs fără variante, dar cu `price` sau `in_stock`, primește automat o variantă `<external_id>#default`.
- `compare_at_price` mai mare decât `price` înseamnă reducere. Scăderea prețului pornește fluxul „Preț redus” pentru cei care au văzut produsul sau l-au pus la favorite în ultimele 30 de zile.
- Trecerea `in_stock` din false în true pornește fluxul „Înapoi în stoc”, pentru aceiași vizitatori.
- `status`: active, draft sau archived. Doar produsele active apar în recomandări.

## Comenzi

POST /orders. Comenzile calculează venitul atribuit campaniilor și fluxurilor, RFM-ul și segmentele după cumpărături.

**POST /orders**

```json
{
  "orders": [
    {
      "external_id": "98123",
      "number": "#10045",
      "customer_external_id": "1042",
      "email": "ana.pop@exemplu.ro",
      "currency": "RON",
      "discount_total": "34.90",
      "shipping_total": "19.99",
      "total": "334.09",
      "financial_status": "paid",
      "fulfillment_status": "unfulfilled",
      "discount_codes": [
        "BINEAIVENIT10"
      ],
      "source_name": "web",
      "placed_at": "2026-11-03T20:15:00+02:00",
      "lines": [
        {
          "product_external_id": "4421",
          "variant_external_id": "4421-40",
          "sku": "URB-40",
          "title": "Sneakers Urban Alb",
          "variant_title": "40",
          "quantity": 1,
          "price": "349.00"
        }
      ]
    }
  ]
}
```

| Câmp | Valori |
| --- | --- |
| financial_status | pending, paid, partially_paid, partially_refunded, refunded, voided |
| fulfillment_status | unfulfilled, partial, fulfilled |
| subtotal | Opțional: dacă lipsește, e suma liniilor (preț × cantitate) |
| refunded_total, cancelled_at | Trimite din nou comanda când se returnează sau se anulează; venitul se corectează |

Comanda se leagă de client după customer_external_id, apoi după email, apoi după telefon. O comandă nouă scoate clientul din fluxurile care se opresc la comandă (de exemplu coș abandonat și produse vizitate).

## Evenimente de pe server

POST /events, pentru ce se întâmplă pe server (de exemplu hook-uri PHP). Pentru navigarea de pe site folosește scriptul; e mai simplu și leagă singur vizitatorul de client.

**POST /events**

```json
{
  "events": [
    {
      "type": "view_product",
      "email": "ana.pop@exemplu.ro",
      "product_external_id": "4421",
      "url": "https://magazin.ro/sneakers-urban-alb"
    },
    {
      "type": "add_to_cart",
      "contact_external_id": "1042",
      "product_external_id": "4421",
      "value": "349.00",
      "props": {
        "quantity": 1
      }
    },
    {
      "type": "search",
      "vid": "v_8f2a91c0d1",
      "props": {
        "query": "sneakers albi"
      },
      "at": "2026-11-04T10:02:00+02:00"
    }
  ]
}
```

| Câmp | Descriere |
| --- | --- |
| type | page_view, view_product, view_category, search, add_to_cart, remove_from_cart, add_to_wishlist, remove_from_wishlist, initiate_checkout, checkout_completed, order, identify |
| vid / contact_external_id / email / phone | Cel puțin unul, ca să știm al cui e evenimentul. `vid` e cookie-ul `ckm_vid` de pe site |
| product_external_id, value, url | Opționale |
| props | Obiect liber, cel mult 4 KB (de exemplu `query`, `quantity`, `cart`). `props.sid` leagă evenimentul de sesiunea de pe site |
| at | Momentul evenimentului; dacă lipsește, acum |

## Ștergeri

POST /contacts/delete, /products/delete și /orders/delete primesc o listă de cel mult 500 de external_id. Fiecare ștergere rămâne în jurnalul contului.

**POST /contacts/delete**

```json
{
  "external_ids": [
    "1042"
  ],
  "reason": "gdpr"
}
```

**Răspuns**

```json
{
  "deleted": 1
}
```

Contactul se șterge cu tot istoricul lui. Comenzile rămân în rapoarte, fără date personale. Cu reason „gdpr” (sau „erasure”, „forget”), emailul și telefonul intră și pe lista de excludere: nu mai primesc nimic, nici dacă revin în listă. Fără motiv (de exemplu client șters din magazin), aceeași persoană se poate abona din nou.

## Scriptul de pe site

Un singur script pentru evenimente, formulare, pop-up-uri și notificări push. Shopify și WooCommerce îl pun singure; pe alt site îl copiezi din Setări → General.

**Înainte de </head>**

```html
<script>window.ckmSettings = { k: "pk_…" };</script>
<script async src="https://marketing.conekto.app/t/s.js?k=pk_…"></script>
```

Scriptul numără singur paginile văzute și sesiunile. Restul evenimentelor le trimiți cu funcția ckm:

**Evenimente din pagină**

```js
ckm("track", "view_product", { product_id: "4421" });
ckm("track", "add_to_cart", { product_id: "4421", props: { cart: { value: 349, items: 1, l: [{ p: "4421", q: 1, v: "4421-40" }] } } });
ckm("track", "search", { props: { query: "sneakers" } });
ckm("identify", { email: "ana.pop@exemplu.ro", src: "form" });
```

| Setare în ckmSettings | Efect |
| --- | --- |
| `consent: false` | Nu trimite nimic până nu schimbi în true (pentru bannerul de cookie-uri) |
| `cart: {...}` | Coșul curent, în formatul de mai sus, pe orice pagină |
| `dataLayer: false` | Oprește preluarea evenimentelor vechi din dataLayer |

> identify cu src „form” leagă vizitatorul de un email scris pe site (cont, contact, checkout), dar nu îl abonează. Abonarea vine doar din formularele noastre sau din acordul trimis prin API.

## Evenimente vechi din dataLayer

Dacă site-ul trimite deja evenimente __sm__* în dataLayer (de la platforma de marketing folosită înainte), scriptul nostru le preia singur. Nu trebuie schimbat nimic pe site.

| Eveniment în dataLayer | Devine | Observații |
| --- | --- | --- |
| `__sm__set_email` | `identify` | emailul scris pe site; nu e acord de marketing |
| `__sm__view_homepage` | `page_view` | pagina se numără oricum automat |
| `__sm__view_category` | `view_category` | calea categoriei, ca în feed („A\|B\|C”) |
| `__sm__view_brand` | `page_view` | pagina de brand |
| `__sm__view_product` | `view_product` | id-ul produsului din feed |
| `__sm__add_to_cart` | `add_to_cart` | produs, cantitate, variantă |
| `__sm__remove_from_cart` | `remove_from_cart` | produs, cantitate, variantă |
| `__sm__add_to_wishlist` | `add_to_wishlist` | produs, variantă |
| `__sm__remove_from_wishlist` | `remove_from_wishlist` | produs, variantă |
| `__sm__initiate_checkout` | `initiate_checkout` | intrarea în checkout |
| `__sm__search` | `search` | termenul căutat |
| `__sm__order` | `identify` | comanda vine din platformă, API sau feed; de pe pagină luăm doar emailul |

Vezi și pagina Setări → Mutare de pe altă platformă din aplicație: acolo urmărești pas cu pas ce date au ajuns.

## Feed de produse și de comenzi

Pentru mutare sau pentru platformele fără API: un feed XML de produse, citit zilnic, și un feed JSON de comenzi, citit pe pagini. Le configurezi în Setări → Mutare.

**Feed XML de produse**

```text
<products>
  <product>
    <id>4421</id>
    <sku>URB</sku>
    <name><![CDATA[Sneakers Urban Alb]]></name>
    <url>https://magazin.ro/sneakers-urban-alb</url>
    <main_image>https://magazin.ro/img/4421.jpg</main_image>
    <category><![CDATA[Încălțăminte|Sneakers]]></category>
    <brand>Urban</brand>
    <price>399</price>
    <sale_price>349</sale_price>
    <availability>1</availability>
    <stock>6</stock>
    <variations>
      <variation><id>4421-40</id><sku>URB-40</sku><price>399</price><sale_price>349</sale_price><size>40</size><availability>1</availability><stock>6</stock></variation>
    </variations>
  </product>
</products>
```

- Categoria are nivelurile despărțite de „|”; fiecare nivel devine o categorie.
- availability: 0 = stoc epuizat, 1 sau 2 = în stoc.
- sale_price contează doar între sale_price_start_date și sale_price_end_date, dacă sunt date (ora României).
- Sumele pot fi „1.234,50” sau „1234.50”.

**Feed JSON de comenzi (o pagină)**

```json
{
  "orders": {
    "order": [
      {
        "order_no": "10045",
        "order_status": "Livrată",
        "created_at": "2026-11-03 20:15",
        "email_address": "ana.pop@exemplu.ro",
        "phone": "0722123456",
        "firstname": "Ana",
        "lastname": "Pop",
        "discount_code": "BINEAIVENIT10",
        "discount_value": "34.90",
        "shipping": "19.99",
        "total_value": "334.09",
        "refund_value": "0",
        "products": [
          {
            "product_id": "4421",
            "name": "Sneakers Urban Alb",
            "variation_sku": "URB-40",
            "price": "399",
            "sale_price": "349",
            "quantity": 1
          }
        ]
      }
    ]
  }
}
```

Citim ?page=1, 2, 3… până la o pagină goală. Sunt acceptate și forma { orders: [...] } și o listă simplă.

## Webhooks

Primești pe serverul tău, în timp real, ce se întâmplă în cont. Le adaugi în Setări → Webhooks, unde vezi și ultimele livrări și poți trimite un test.

| Eveniment | Când |
| --- | --- |
| `contact.subscribed` | Un contact s-a abonat la email sau SMS (formular, pop-up, confirmare). |
| `contact.unsubscribed` | Un contact s-a dezabonat sau adresa a fost curățată. |
| `form.submitted` | Un formular sau pop-up a fost trimis, cu câmpurile completate. |
| `campaign.sent` | O campanie a terminat trimiterea, cu numărul de destinatari. |
| `transactional.sent` | Un email sau SMS din API ori SMTP a plecat la furnizor. |
| `transactional.delivered` | Furnizorul confirmă că mesajul a ajuns la destinatar. |
| `transactional.bounced` | Adresa sau numărul a respins mesajul (bounce). |
| `transactional.complained` | Destinatarul a marcat emailul ca spam. |
| `transactional.failed` | Mesajul nu a plecat: adresă exclusă, mod de test, credit sau eroare. |

**Corpul cererii (POST, JSON)**

```json
{
  "id": "whd_…",
  "event": "contact.subscribed",
  "createdAt": "2026-11-04T08:12:31.000Z",
  "shop": "magazin.ro",
  "data": {
    "id": "…",
    "email": "ana.pop@exemplu.ro",
    "emailStatus": "SUBSCRIBED",
    "smsStatus": "NOT_SUBSCRIBED",
    "whatsappStatus": "NOT_SUBSCRIBED",
    "tags": [],
    "source": "form"
  }
}
```

| Antet | Conținut |
| --- | --- |
| X-Conekto-Event | Numele evenimentului |
| X-Conekto-Delivery | Id-ul livrării; îl folosești ca să nu procesezi de două ori |
| X-Conekto-Timestamp | Secunde de la 1970 |
| X-Conekto-Signature | `sha256=` + HMAC SHA-256 cu secretul webhook-ului, peste `<timestamp>.<corp>` |

**Verificarea semnăturii (PHP)**

```php
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CONEKTO_TIMESTAMP'];
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('CONEKTO_WEBHOOK_SECRET'));
if (!hash_equals($expected, $_SERVER['HTTP_X_CONEKTO_SIGNATURE']) || abs(time() - (int)$ts) > 300) {
  http_response_code(401); exit;
}
```

- Răspunde cu un cod 2xx în cel mult 10 secunde; altfel livrarea se reia de până la 5 ori, la intervale tot mai mari.
- După 50 de eșecuri la rând, webhook-ul se oprește singur și îl repornești din Setări.
- Doar adrese https publice; istoricul livrărilor se păstrează 30 de zile.

## Mediu de test

Faci integrarea fără ca datele de probă să ajungă în contacte, comenzi sau rapoarte.

- Cheile `ck_test_…` răspund exact ca cele reale (aceleași validări, aceleași erori pe obiecte), dar nu salvează și nu șterg nimic. Răspunsul are `"test": true`.
- Domeniile de test (Setări → Mediu de test): scriptul pus pe staging trimite evenimente care nu se salvează și nu pornesc automatizări.
- Ce primim de la cheile și domeniile de test apare în jurnalul din aceeași pagină, în câteva secunde (ultimele 100 de cereri, 24 de ore).

**Un contact trimis cu cheia de test**

```bash
curl -X POST https://marketing.conekto.app/api/v1/contacts \
  -H "Authorization: Bearer ck_test_…" \
  -H "Content-Type: application/json" \
  -d '{"contacts":[{"external_id":"42","email":"ion@exemplu.ro","email_consent":{"state":"subscribed"}}]}'
```

**Răspuns**

```json
{
  "received": 1,
  "created": 1,
  "updated": 0,
  "errors": [],
  "test": true,
  "note": "Cheie de test: datele au fost verificate, dar nu au fost salvate."
}
```

## Documentația pentru asistenți AI

Toată documentația, ca un singur fișier text, pentru Claude, ChatGPT sau Cursor.

- `/llms.txt`: rezumatul și legăturile, după standardul llms.txt.
- `/dezvoltatori.md`: toată documentația într-un singur fișier Markdown.
- Conectorul Claude (MCP) din aplicație: Claude lucrează direct în cont (campanii, segmente, rapoarte).

## Ce s-a schimbat

### 2026-11-04 · Mediu de test și mutare de pe altă platformă
- Chei API de test (ck_test_…): aceleași validări, nimic salvat; răspunsul are „test”: true.
- Domenii de test pentru script: evenimentele de pe staging nu se salvează și apar în jurnalul de test.
- Scriptul preia evenimentele vechi din dataLayer (__sm__*), fără schimbări pe site.
- Import din feed XML de produse (zilnic) și feed JSON de comenzi, pe pagini.

### 2026-10-15 · Webhooks
- 4 evenimente: abonare, dezabonare, formular trimis, campanie trimisă. Semnătură HMAC SHA-256, 5 reîncercări.

### 2026-09-01 · API v1
- Contacte, produse, categorii, comenzi, evenimente și ștergeri, în loturi de până la 500 de obiecte.
