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 ) )
Κανόνες:
- Υπολόγισε την υπογραφή πάνω στο ακατέργαστο σώμα (raw bytes), πριν από JSON parsing.
- Σύγκρινε με σταθερού χρόνου σύγκριση (constant-time).
- Απόρριψε αιτήματα με timestamp παλαιότερο από λίγα λεπτά (π.χ. 5), για προστασία από replay.
- Απάντησε γρήγορα με
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.