Skip to content
Syncore

Webhooks

Τα webhooks στέλνουν ένα υπογεγραμμένο POST στη διεύθυνσή σου όταν συμβαίνει κάτι στον λογαριασμό σου, ώστε να μη χρειάζεται να ρωτάς συνέχεια το API.

Συμβάντα

Συμβάν Πότε data
order.created Νέα παραγγελία από οποιοδήποτε κανάλι Η παραγγελία, όπως στο GET /orders/{id} (με items)
order.updated Αλλαγή παραγγελίας (κατάσταση, αποστολή, …) Η παραγγελία, όπως στο GET /orders/{id}
product.updated Αλλαγή ενεργού / τιμής προϊόντων product_ids, change (price, enabled, disabled)
stock.changed Αλλαγή του δικού σου αποθέματος ή των δεσμευμένων product_id, sku, quantity, quantity_before, reserved
stock.low Το απόθεμα έπεσε στο όριο χαμηλού αποθέματος (μία φορά, όχι σε κάθε αλλαγή κάτω από αυτό) ό,τι το stock.changed + threshold
feed.generated Νέο αρχείο feed για ένα κανάλι channel, trigger, products_count, file_size, feed_url, generated_at

Το όριο χαμηλού αποθέματος ορίζεται στις ρυθμίσεις του λογαριασμού· με όριο 0 το stock.low δεν στέλνεται. Το κουμπί δοκιμής στη σελίδα Webhooks στέλνει συμβάν ping.

Μορφή αποστολής

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Syncore-Webhooks/1
X-Syncore-Event: stock.low
X-Syncore-Delivery: 2f1c7a4e-7c55-4c1e-9d0b-1b9b2f0c3a10
X-Syncore-Timestamp: 1761724800
X-Syncore-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
{
  "id": "2f1c7a4e-7c55-4c1e-9d0b-1b9b2f0c3a10",
  "event": "stock.low",
  "created_at": "2026-10-29T10:00:00+02:00",
  "data": {
    "product_id": 1201,
    "sku": "ABC-1001",
    "quantity": 3,
    "quantity_before": 6,
    "reserved": 1,
    "threshold": 5
  }
}

Το id είναι ίδιο με το X-Syncore-Delivery και μένει ίδιο στις επαναλήψεις — χρησιμοποίησέ το για να αγνοείς διπλές παραλαβές.

Παράδειγμα feed.generated:

{
  "id": "8a0d3b8e-3f5e-4bde-a0f0-5b3c1f0d9e21",
  "event": "feed.generated",
  "created_at": "2026-10-29T10:00:00+02:00",
  "data": {
    "channel": "skroutz",
    "trigger": "scheduled",
    "products_count": 340,
    "file_size": 182340,
    "feed_url": "https://app.syncore.gr/feeds/your-shop/skroutz",
    "generated_at": "2026-10-29T10:00:00+02:00"
  }
}

Επαλήθευση υπογραφής

Η υπογραφή είναι HMAC-SHA256 με το secret του endpoint πάνω στο "{timestamp}.{raw body}":

X-Syncore-Signature = "sha256=" + hex( HMAC_SHA256( secret, X-Syncore-Timestamp + "." + raw_body ) )

Κανόνες:

  1. Υπολόγισε την υπογραφή πάνω στο ακατέργαστο σώμα (raw bytes), πριν από JSON parsing.
  2. Σύγκρινε με σταθερού χρόνου σύγκριση (constant-time).
  3. Απόρριψε αιτήματα με timestamp παλαιότερο από λίγα λεπτά (π.χ. 5), για προστασία από replay.
  4. Απάντησε γρήγορα με 2xx και κάνε τη βαριά δουλειά ασύγχρονα.

PHP

<?php
$secret    = getenv('SYNCORE_WEBHOOK_SECRET');
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_SYNCORE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SYNCORE_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (! hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// ... επεξεργασία $event['event'], $event['data']
http_response_code(204);

Node.js (Express)

import express from 'express';
import crypto from 'node:crypto';

const app = express();

app.post('/syncore/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Syncore-Timestamp') ?? '';
  const signature = req.get('X-Syncore-Signature') ?? '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.SYNCORE_WEBHOOK_SECRET)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');

  const valid = signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body);
  // ... επεξεργασία event.event, event.data
  res.sendStatus(204);
});

Python (Flask)

import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/syncore/webhook")
def syncore_webhook():
    body = request.get_data()  # raw bytes
    timestamp = request.headers.get("X-Syncore-Timestamp", "")
    signature = request.headers.get("X-Syncore-Signature", "")
    secret = os.environ["SYNCORE_WEBHOOK_SECRET"].encode()

    expected = "sha256=" + hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature) or abs(time.time() - int(timestamp or 0)) > 300:
        abort(401)

    event = request.get_json()
    # ... επεξεργασία event["event"], event["data"]
    return "", 204

Επαναλήψεις

  • Επιτυχία είναι κάθε απάντηση 2xx μέσα σε 10 δευτερόλεπτα. Ανακατευθύνσεις (3xx) δεν ακολουθούνται και μετρούν ως αποτυχία.
  • Μετά από αποτυχία, νέες προσπάθειες σε 1 λεπτό, 5 λεπτά, 30 λεπτά και 2 ώρες (5 προσπάθειες συνολικά). Μετά η αποστολή σημειώνεται ως αποτυχημένη.
  • Από τη σελίδα API & Webhooks του Syncore βλέπεις το ιστορικό αποστολών και ξαναστέλνεις χειροκίνητα μια αποτυχημένη (με το ίδιο id).
  • Η σειρά παράδοσης δεν είναι εγγυημένη: για παραγγελίες βασίσου στο updated_at της παραγγελίας.

Διαχείριση endpoints

Endpoints φτιάχνεις από τη σελίδα API & Webhooks του Syncore ή μέσω API με token που έχει webhooks:manage. Η διεύθυνση πρέπει να είναι δημόσια https:// (όχι localhost ή ιδιωτικές IP). Έως 10 endpoints ανά επιχείρηση.

Νέο endpoint

curl -s -X POST "https://app.syncore.gr/api/v1/webhooks" \
  -H "Authorization: Bearer $SYNCORE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/syncore/webhook", "events": ["order.created", "order.updated", "stock.low"]}'
{
  "data": {
    "id": 12,
    "url": "https://example.com/syncore/webhook",
    "events": ["order.created", "order.updated", "stock.low"],
    "is_active": true,
    "last_status": null,
    "last_delivered_at": null,
    "secret": "whsec_..."
  }
}

Το secret εμφανίζεται μόνο σε αυτή την απάντηση. Αποθήκευσέ το αμέσως.

Λίστα και διαγραφή

curl -s "https://app.syncore.gr/api/v1/webhooks" -H "Authorization: Bearer $SYNCORE_TOKEN"
curl -s -X DELETE "https://app.syncore.gr/api/v1/webhooks/12" -H "Authorization: Bearer $SYNCORE_TOKEN"   # 204

Αλλαγή secret (rotate)

curl -s -X POST "https://app.syncore.gr/api/v1/webhooks/12/rotate-secret" -H "Authorization: Bearer $SYNCORE_TOKEN"

Επιστρέφει το endpoint με νέο secret (μία φορά). Το παλιό secret σταματά να ισχύει αμέσως, οπότε ενημέρωσε πρώτα τον κώδικά σου ώστε να δέχεται και τα δύο για λίγο, και μετά κάνε rotate.