Parametri
- SHOPENTLY_SHOP_URL — URL tar-riżorsa.
- SHOPENTLY_ACCESS_TOKEN — Kredenzjal Bearer b’validità qasira.
- CREATED_FROM — Bidu tal-filtru tal-ħin tal-ħolqien.
- CREATED_TO — Tmiem tal-filtru tal-ħin tal-ħolqien.
PHP 8.3 (64-bit, ext-curl, JSON) · Python 3.12 + requests · cURL 8 + jq 1.7.
Eżempju sħiħ
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
Kif jaħdem
Il-lista u d-dettalji tal-ordnijiet jinkludu delivery.dates. Id-dati YYYY-MM-DD huma ppjanati u ma jikkonfermawx it-tluq. Id-dati attwali jieħdu preċedenza; ordnijiet qodma jużaw l-istima ssejvjata. Valuri mħassra jew mhux magħrufa huma null. Id-dettalji jinkludu wkoll il-linji kollha u l-immaġnijiet tal-prodotti.
Ikseb identifikaturi validi tal-istatus minn /api/v1/order-statuses. L-istatus tal-ordni u tal-ħlas jużaw endpoints PATCH u permessi separati. Il-bidla tal-istatus tal-ħlas irreġistrat ma tiġborx ħlas minn karta u ma tagħmilx rifużjoni permezz ta’ fornitur tal-ħlas.
delivery.dates: planned_dispatch_on, dispatch_timezone, estimated_delivery_from, estimated_delivery_to. Dati tat-tluq u tal-kunsinna: YYYY-MM-DD; null. Żona tal-ħin tat-tluq: IANA.
Ordni · Totali
L-ammonti u r-rati jiġu mill-ordni maħżuna, inklużi l-korrezzjonijiet tal-linji. Id-dejta storika nieqsa hija null u ma tinbidilx bir-rati attwali tal-katalgu. L-ammonti jibqgħu strings deċimali.
- tax_country_code · Pajjiż tat-taxxa · Dejta maħżuna tal-ordni · ISO 3166-1 alpha-2; null.
- lines[].unit_* · Unità waħda · Qabel l-iskontijiet; lines[].original_total_* · Qabel l-iskontijiet; lines[].total_* · Wara l-iskontijiet.
- lines[].discount_* · Skontijiet; manual_discount · Skont manwali; percentage "10" = 10%.
- delivery.net / tax / gross · Wara l-iskontijiet; delivery.original_* · Qabel l-iskontijiet; delivery.discount_* · Skontijiet.
- tax_rate: "0.23" = 23%; "0" ≠ null. Mhux magħruf: null; żero magħruf: 0.
- totals · Totali; tax_breakdown.groups · Gruppi tat-taxxa; Taxxa mhux allokata = order.tax − Σ groups.tax; ±.
Il-lista tagħti sommarji. Ikseb GET /api/v1/orders/{order_id} għal kull riżultat biex tesporta d-dettalji finanzjarji sħaħ. It-tentattivi GET huma limitati u jirrispettaw Retry-After. Esportazzjoni skont id-data tal-ħolqien ma tissinkronizzax korrezzjonijiet sussegwenti.
payment.amount huwa t-total tal-ordni. L-istatus tal-ħlas ma jiddeterminax ammonti mħallsa, rimborżati jew dovuti.