Παράμετροι
- SHOPENTLY_SHOP_URL — URL πόρου.
- SHOPENTLY_ACCESS_TOKEN — Διαπιστευτήριο Bearer μικρής διάρκειας.
- CREATED_FROM — Αρχή του φίλτρου χρόνου δημιουργίας.
- CREATED_TO — Τέλος του φίλτρου χρόνου δημιουργίας.
PHP 8.3 (64-bit, ext-curl, JSON) · Python 3.12 + requests · cURL 8 + jq 1.7.
Πλήρες παράδειγμα
cURL
#!/bin/sh
# POSIX shell, curl 8+, jq 1.7. Credentials stay in environment variables.
set -eu
: "${SHOPENTLY_SHOP_URL:?}"
base=${SHOPENTLY_SHOP_URL%/}
case "$base" in https://*) ;; *) echo 'Use an HTTPS origin' >&2; exit 1;; esac
authority=${base#https://}
case "$authority" in ''|*/*|*\?*|*\#*|*@*) echo 'Use an HTTPS origin' >&2; exit 1;; esac
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
chmod 700 "$work"
encode() { printf '%s' "$1" | jq -sRr @uri; }
api() {
method=$1; path=$2; body=${3-}; key=${4-}
scheme=Bearer; credential=${SHOPENTLY_ACCESS_TOKEN-}
if [ "$path" = /api/v1/auth/token ]; then scheme=ApiKey; credential=${SHOPENTLY_API_KEY-}; fi
[ -n "$credential" ] || { echo 'Missing credential' >&2; return 1; }
set -- --disable --globoff --silent --show-error --proto '=https' --connect-timeout 5 --max-time 30 \
--request "$method" --header "Authorization: $scheme $credential" \
--dump-header "$work/headers" --output "$work/body" --write-out '%{http_code}'
if [ -n "$key" ]; then set -- "$@" --header "Idempotency-Key: $key"; fi
if [ -n "$body" ]; then set -- "$@" --header 'Content-Type: application/json' --data-raw "$body"; fi
if [ "$method" = GET ]; then set -- "$@" --retry 2 --retry-max-time 60; fi
status=$(curl "$@" --url "$base$path") || return 1
case "$status" in
2??) if [ -s "$work/body" ]; then jq -e . "$work/body"; else printf 'null\n'; fi ;;
*) printf 'HTTP %s\n' "$status" >&2
jq -c '.error | {code, fields}' "$work/body" >&2 2>/dev/null || true
return 1 ;;
esac
}
query='limit=100'
: "${CREATED_FROM:?}" "${CREATED_TO:?}"
query="$query&created_from=$(encode "$CREATED_FROM")&created_to=$(encode "$CREATED_TO")"
seen='[]'
while :; do
page=$(api GET "/api/v1/orders?$query")
printf '%s' "$page" | jq -r '.data[].id | @uri' > "$work/order-ids"
while IFS= read -r order_id; do
detail=$(api GET "/api/v1/orders/$order_id")
printf '%s' "$detail" | jq -c '.data'
done < "$work/order-ids"
more=$(printf '%s' "$page" | jq -r '.meta.pagination.has_more')
[ "$more" = true ] || break
cursor=$(printf '%s' "$page" | jq -er '.meta.pagination.next_cursor')
if printf '%s' "$seen" | jq -e --arg cursor "$cursor" 'index($cursor) != null' >/dev/null; then
echo 'Repeated cursor' >&2; exit 1
fi
seen=$(printf '%s' "$seen" | jq -c --arg cursor "$cursor" '. + [$cursor]')
query="limit=100&cursor=$(encode "$cursor")"
query="$query&created_from=$(encode "$CREATED_FROM")&created_to=$(encode "$CREATED_TO")"
done
PHP
<?php
declare(strict_types=1);
set_exception_handler(static function (Throwable $error): void {
fwrite(STDERR, $error->getMessage() . PHP_EOL);
exit(1);
});
// PHP 8.3+, 64-bit, ext-curl. Credentials stay in environment variables.
function env(string $name): string {
$value = getenv($name);
if ($value === false || $value === '' || strpbrk($value, "\r\n") !== false) {
throw new RuntimeException('Invalid environment variable: ' . $name);
}
return $value;
}
$base = rtrim(env('SHOPENTLY_SHOP_URL'), '/');
$origin = parse_url($base);
if (!$origin || ($origin['scheme'] ?? '') !== 'https' || empty($origin['host']) ||
isset($origin['user']) || isset($origin['pass']) || isset($origin['path']) ||
isset($origin['query']) || isset($origin['fragment']) || PHP_INT_SIZE !== 8) {
throw new RuntimeException('Use a 64-bit PHP runtime and an HTTPS origin');
}
class ApiError extends RuntimeException {
public function __construct(public int $status, public array $headers, public array $error) {
parent::__construct('HTTP ' . $status . '; code=' . ($error['code'] ?? '') .
'; request_id=' . ($headers['x-request-id'] ?? ''));
}
}
function api(string $method, string $path, array $query = [], ?array $body = null, ?string $key = null, bool $apiKey = false): ?array {
global $base;
$credential = env($apiKey ? 'SHOPENTLY_API_KEY' : 'SHOPENTLY_ACCESS_TOKEN');
$headers = ['Authorization: ' . ($apiKey ? 'ApiKey ' : 'Bearer ') . $credential];
if ($key !== null) $headers[] = 'Idempotency-Key: ' . $key;
if ($body !== null) $headers[] = 'Content-Type: application/json';
$url = $base . $path . ($query ? '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986) : '');
$responseHeaders = [];
$h = curl_init($url);
curl_setopt_array($h, [
CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 30,
CURLOPT_HEADERFUNCTION => static function ($h, string $line) use (&$responseHeaders): int {
if (str_contains($line, ':')) {
[$name, $value] = explode(':', $line, 2);
$responseHeaders[strtolower(trim($name))] = trim($value);
}
return strlen($line);
},
]);
if ($body !== null) {
$json = $apiKey && $body === [] ? '{}' : json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
curl_setopt($h, CURLOPT_POSTFIELDS, $json);
}
$raw = curl_exec($h);
$status = curl_getinfo($h, CURLINFO_RESPONSE_CODE);
$error = curl_error($h);
curl_close($h);
if ($raw === false) throw new RuntimeException('Transport error: ' . $error);
if ($status < 200 || $status >= 300) {
$data = json_decode($raw, true);
throw new ApiError($status, $responseHeaders, is_array($data) ? ($data['error'] ?? []) : []);
}
return $raw === '' ? null : json_decode($raw, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
}
function output(mixed $value): void {
echo json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), PHP_EOL;
}
function readOrderApi(string $path, array $query = []): array {
$deadline = hrtime(true) / 1e9 + 60;
for ($attempt = 0; $attempt < 3; $attempt++) {
try {
return api('GET', $path, $query);
} catch (ApiError $error) {
if ($attempt === 2 || !in_array($error->status, [408, 429, 500, 502, 503, 504], true)) throw $error;
$delay = 2 ** $attempt;
$retry = $error->headers['retry-after'] ?? '';
if ($retry !== '') {
if (preg_match('/^[0-9]+$/D', $retry) === 1) $delay = (float)$retry;
elseif (($date = strtotime($retry)) !== false) $delay = max(0, $date - time());
}
if ($delay > $deadline - hrtime(true) / 1e9) throw $error;
usleep((int)($delay * 1000000));
}
}
throw new RuntimeException('GET retry limit reached');
}
$query = ['limit' => 100, 'created_from' => env('CREATED_FROM'), 'created_to' => env('CREATED_TO')];
$seen = [];
do {
$page = readOrderApi('/api/v1/orders', $query);
foreach ($page['data'] as $record) {
$detail = readOrderApi('/api/v1/orders/' . rawurlencode($record['id']));
output($detail['data']);
}
$pagination = $page['meta']['pagination'];
if (!$pagination['has_more']) break;
$cursor = $pagination['next_cursor'];
if (!$cursor || isset($seen[$cursor])) throw new RuntimeException('Missing or repeated cursor');
$seen[$cursor] = true;
$query['cursor'] = $cursor;
} while (true);
Python
"""Run with Python 3.12 and requests. Credentials stay in environment variables."""
import json
import os
import time
from urllib.parse import quote, urlsplit
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import requests
def env(name):
value = os.environ[name]
if not value or "\r" in value or "\n" in value:
raise ValueError("Invalid environment variable: " + name)
return value
base = env("SHOPENTLY_SHOP_URL").rstrip("/")
origin = urlsplit(base)
if origin.scheme != "https" or not origin.hostname or origin.username or origin.password or origin.path or origin.query or origin.fragment:
raise ValueError("SHOPENTLY_SHOP_URL must be an HTTPS origin")
class ApiError(RuntimeError):
def __init__(self, response):
self.status = response.status_code
self.request_id = response.headers.get("X-Request-ID", "")
self.retry_after = response.headers.get("Retry-After", "")
try:
self.error = response.json().get("error", {})
except (ValueError, AttributeError):
self.error = {}
super().__init__(f"HTTP {self.status}; code={self.error.get('code', '')}; request_id={self.request_id}")
def api(method, path, *, query=None, body=None, key=None, api_key=False):
credential = env("SHOPENTLY_API_KEY" if api_key else "SHOPENTLY_ACCESS_TOKEN")
headers = {"Authorization": ("ApiKey " if api_key else "Bearer ") + credential}
if key:
headers["Idempotency-Key"] = key
if body is not None:
headers["Content-Type"] = "application/json"
response = requests.request(method, base + path, params=query, headers=headers,
data=json.dumps(body, ensure_ascii=False, allow_nan=False).encode() if body is not None else None,
timeout=(5, 30), allow_redirects=False)
if not 200 <= response.status_code < 300:
raise ApiError(response)
return response.json() if response.content else None
def segment(value):
return quote(value, safe="")
def read(path, query=None):
deadline = time.monotonic() + 60
for attempt in range(3):
try:
return api("GET", path, query=query)
except ApiError as error:
if attempt == 2 or error.status not in {408, 429, 500, 502, 503, 504}:
raise
delay = 2 ** attempt
if error.retry_after:
try:
delay = int(error.retry_after)
except ValueError:
try:
delay = (parsedate_to_datetime(error.retry_after) - datetime.now(timezone.utc)).total_seconds()
except (ValueError, TypeError):
pass
delay = max(0, delay)
if delay > deadline - time.monotonic():
raise
time.sleep(delay)
query = {"limit": 100}
query.update(created_from=env("CREATED_FROM"), created_to=env("CREATED_TO"))
seen = set()
while True:
page = read("/api/v1/orders", query)
for record in page["data"]:
detail = read("/api/v1/orders/" + segment(record["id"]))
print(json.dumps(detail["data"], ensure_ascii=False, allow_nan=False))
pagination = page["meta"]["pagination"]
if not pagination["has_more"]:
break
cursor = pagination["next_cursor"]
if not cursor or cursor in seen:
raise RuntimeError("Missing or repeated cursor")
seen.add(cursor)
query["cursor"] = cursor
Αρχές
Η λίστα και οι λεπτομέρειες παραγγελιών περιλαμβάνουν delivery.dates. Οι ημερομηνίες YYYY-MM-DD είναι προγραμματισμένες και δεν επιβεβαιώνουν αποστολή. Οι τρέχουσες ημερομηνίες υπερισχύουν· οι παλιές παραγγελίες χρησιμοποιούν την αποθηκευμένη εκτίμηση. Διαγραμμένες ή άγνωστες τιμές είναι null. Οι λεπτομέρειες περιλαμβάνουν επίσης όλες τις γραμμές και εικόνες προϊόντων.
Λάβετε έγκυρα αναγνωριστικά κατάστασης από το /api/v1/order-statuses. Η κατάσταση παραγγελίας και η κατάσταση πληρωμής χρησιμοποιούν ξεχωριστά σημεία πρόσβασης PATCH και δικαιώματα. Η αλλαγή της καταγεγραμμένης κατάστασης πληρωμής δεν χρεώνει κάρτα ούτε εκτελεί επιστροφή μέσω παρόχου πληρωμών.
delivery.dates: planned_dispatch_on, dispatch_timezone, estimated_delivery_from, estimated_delivery_to. Ημερομηνίες αποστολής και παράδοσης: YYYY-MM-DD; null. Ζώνη ώρας αποστολής: IANA.
Παραγγελία · Σύνολα
Τα ποσά και οι συντελεστές προέρχονται από την αποθηκευμένη παραγγελία, μαζί με τις διορθώσεις γραμμών. Τα άγνωστα ιστορικά στοιχεία είναι null και δεν αντικαθίστανται από τρέχοντες συντελεστές καταλόγου. Τα ποσά παραμένουν δεκαδικές συμβολοσειρές.
- tax_country_code · Φορολογική χώρα · Αποθηκευμένα στοιχεία παραγγελίας · ISO 3166-1 alpha-2; null.
- lines[].unit_* · Μία μονάδα · Πριν τις εκπτώσεις; lines[].original_total_* · Πριν τις εκπτώσεις; lines[].total_* · Μετά τις εκπτώσεις.
- lines[].discount_* · Εκπτώσεις; manual_discount · Χειροκίνητη έκπτωση; percentage "10" = 10%.
- delivery.net / tax / gross · Μετά τις εκπτώσεις; delivery.original_* · Πριν τις εκπτώσεις; delivery.discount_* · Εκπτώσεις.
- tax_rate: "0.23" = 23%; "0" ≠ null. Άγνωστο: null· γνωστό μηδέν: 0.
- totals · Σύνολα; tax_breakdown.groups · Φορολογικές ομάδες; Μη κατανεμημένος φόρος = order.tax − Σ groups.tax; ±.
Η λίστα επιστρέφει συνόψεις. Ανακτήστε GET /api/v1/orders/{order_id} για κάθε αποτέλεσμα για πλήρη οικονομικά στοιχεία. Οι επαναλήψεις GET είναι περιορισμένες και τηρούν το Retry-After. Η εξαγωγή βάσει ημερομηνίας δημιουργίας δεν συγχρονίζει μεταγενέστερες διορθώσεις.
Το payment.amount είναι το σύνολο παραγγελίας. Η κατάσταση πληρωμής δεν καθορίζει πληρωμένα, επιστραφέντα ή οφειλόμενα ποσά.