Parâmetros
- SHOPENTLY_SHOP_URL — URL do recurso.
- SHOPENTLY_ACCESS_TOKEN — Credencial Bearer de curta duração.
- CREATED_FROM — Início do filtro por momento de criação.
- CREATED_TO — Fim do filtro por momento de criação.
PHP 8.3 (64-bit, ext-curl, JSON) · Python 3.12 + requests · cURL 8 + jq 1.7.
Exemplo completo
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
Como funciona
A lista e os detalhes dos pedidos incluem delivery.dates. As datas YYYY-MM-DD são previstas e não confirmam o envio. As datas atuais têm prioridade; pedidos antigos usam a estimativa salva. Valores apagados ou desconhecidos são null. Os detalhes incluem também todas as linhas e imagens dos produtos.
Obtenha identificadores de estado válidos em /api/v1/order-statuses. O estado do pedido e o estado do pagamento usam endpoints PATCH e permissões separados. Alterar o estado de pagamento registrado não cobra um cartão nem emite um reembolso por um provedor de pagamentos.
delivery.dates: planned_dispatch_on, dispatch_timezone, estimated_delivery_from, estimated_delivery_to. Datas de envio e entrega: YYYY-MM-DD; null. Fuso horário do envio: IANA.
Pedido · Totais
Os valores e alíquotas vêm do pedido salvo, incluindo correções dos itens. Dados históricos ausentes são null e não são substituídos pelas alíquotas atuais do catálogo. Os valores continuam sendo textos decimais.
- tax_country_code · País fiscal · Dados salvos do pedido · ISO 3166-1 alpha-2; null.
- lines[].unit_* · Uma unidade · Antes dos descontos; lines[].original_total_* · Antes dos descontos; lines[].total_* · Após os descontos.
- lines[].discount_* · Descontos; manual_discount · Desconto manual; percentage "10" = 10%.
- delivery.net / tax / gross · Após os descontos; delivery.original_* · Antes dos descontos; delivery.discount_* · Descontos.
- tax_rate: "0.23" = 23%; "0" ≠ null. Desconhecido: null; zero conhecido: 0.
- totals · Totais; tax_breakdown.groups · Grupos fiscais; Imposto não atribuído = order.tax − Σ groups.tax; ±.
A lista retorna resumos. Obtenha GET /api/v1/orders/{order_id} para cada resultado para exportar o detalhamento financeiro completo. As tentativas GET são limitadas e respeitam Retry-After. A exportação por data de criação não sincroniza correções posteriores.
payment.amount é o total do pedido. O status do pagamento não determina valores pagos, reembolsados ou pendentes.