Traé tus pedidos a tu propio sistema sin exportar el CSV a mano: número, juego, plataforma, licencia, importe en ARS y USDT, fechas y estado. Con sincronización incremental, así cada corrida te trae solo lo que cambió. Las entregas activas incluyen correo, contraseña y el último código entregado a ese pedido.
partner_orders_v1 · REST · JSON · solo lectura → ¿Buscabas la API de catálogo (precios y stock)?| Dato | Valor |
|---|---|
| URL base | https://mayoristas.epicodes.com.ar/partner-api.phpEl mismo dominio que la API de catálogo. |
| Método | GET |
| API Key | Te la pasamos por privado. Va en el header X-Api-Key. |
?api_key=... queda escrito en los logs de acceso, en el historial
del navegador y en la cabecera Referer de cualquier link que se
abra desde ahí. Esta API se llama desde tu servidor, no desde el navegador
de tus clientes.
| Endpoint | Para qué |
|---|---|
GET /ping | Probar la key: te dice de quién es, qué alcance tiene y qué cuentas incluye. |
GET /orders | La lista de pedidos, paginada. |
GET /orders/{sale_number} | Un pedido puntual, por su número. |
Empezá por /ping: confirma que la key anda y te muestra qué
cuentas vas a recibir.
curl "https://mayoristas.epicodes.com.ar/partner-api.php/ping" \
-H "X-Api-Key: TU_API_KEY"
curl "https://mayoristas.epicodes.com.ar/partner-api.php/orders?limit=100" \
-H "X-Api-Key: TU_API_KEY"
const r = await fetch(
'https://mayoristas.epicodes.com.ar/partner-api.php/orders?order=updated&limit=200',
{ headers: { 'X-Api-Key': process.env.EPICODES_API_KEY } }
)
const data = await r.json()
if (!r.ok) throw new Error(data.error) // el status HTTP es real: 401, 403, 429...
for (const p of data.orders) {
// guardalo con UPSERT por sale_number (ver seccion 6)
}
$ch = curl_init('https://mayoristas.epicodes.com.ar/partner-api.php/orders?limit=200');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('EPICODES_API_KEY')],
]);
$resp = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
{
"ok": true,
"api": "partner_orders_v1",
"scope": "propios_y_subs",
"count": 100,
"has_more": true,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0wMlQwODowMDowMC4wMDBaIiwiaSI6NDgyMX0",
"synced_through": "2026-09-09T14:22:31.000Z",
"server_time": "2026-09-09T14:22:36.000Z",
"orders": [ ... ]
}
| Campo | Qué es |
|---|---|
count | Cuántos pedidos trae esta página. |
has_more | true si quedan más. Seguí pidiendo con next_cursor hasta que sea false. |
next_cursor | Dónde sigue el recorrido. Es opaco: guardalo tal cual, no lo interpretes. Puede cambiar de forma sin aviso. |
synced_through | Hasta qué instante está garantizado el corte. Todo lo anterior a esta marca ya te lo dimos. |
| Campo | Tipo | Descripción |
|---|---|---|
sale_number | int | Usá este como clave primaria. Es el número de pedido, único y siempre presente. |
order_id | string|null | Identificador interno. Puede venir en null en pedidos históricos. |
status | string | PENDING, FULFILLED o REFUNDED. Ver sección 5. |
order_id como clave. Es
nullable: los pedidos más viejos no lo tienen. Si lo usás como primary key, todo tu
histórico colapsa en una sola fila con clave nula. La clave es
sale_number.
| Campo | Tipo | Descripción |
|---|---|---|
title | string | Nombre del juego. |
platform | string|null | PS4 o PS5. |
license | string|null | Primario o Secundario. |
| Campo | Tipo | Descripción |
|---|---|---|
total_ars | number | Lo que pagaste por ese pedido, en pesos. |
total_usdt | number | Lo mismo en USDT. |
currency | string|null | Con cuál de las dos se cobró: ARS o USDT. |
Los dos importes vienen siempre. currency te dice
cuál es el que se cobró de verdad; el otro es la conversión.
| Campo | Tipo | Descripción |
|---|---|---|
created_at | string | Cuándo se hizo el pedido. No cambia nunca. |
fulfilled_at | string|null | Cuándo se entregó. |
refunded_at | string|null | Cuándo se devolvió, si se devolvió. |
updated_at | string | Último cambio. Es el eje de la sincronización (sección 6). |
Disponibles en /orders y
/orders/{sale_number}, dentro del mismo alcance de tu clave.
| Campo | Tipo | Descripción |
|---|---|---|
delivery | object|null | Solo entregas activas. Es null en pendientes, devoluciones, pedidos reemplazados o sin datos de entrega. |
delivery.email_psn | string | Correo de la cuenta de PlayStation. |
delivery.password_psn | string | Contraseña resuelta con las mismas reglas de “Ver Datos” del portal. |
delivery.codigo_unico | string | Último código ya entregado a este pedido, o vacío si no tiene. Consultarlo no consume ni genera otro código; tampoco garantiza que el cliente no lo haya usado. |
delivery.nombre_psn / delivery.psn_id | string | Nombre del perfil e identificador PSN, si están disponibles. |
aviso_cuenta | object|null | { tipo, texto }. bloqueado: no entregar y contactar soporte; se conserva la contraseña histórica y no se publica la nueva. password_cambiada: usar la contraseña actual de esta respuesta. |
credentials_checked_at | string|null | Momento de consulta de las credenciales (ISO UTC); null si no hay entrega activa. |
updated_at del pedido: el cursor incremental
sincroniza pedidos, no garantiza refrescar credenciales. No uses una copia guardada para
entregar. Si aviso_cuenta.tipo === 'bloqueado', detené la entrega y contactá soporte.
Las respuestas llevan Cache-Control: no-store. Guardá la API key
en tu servidor; no la publiques ni registres las credenciales en logs.
| Campo | Tipo | Descripción |
|---|---|---|
order_source | string|null | instant, cart, replacement, sin_cargo, admin. |
replacement_for_order_id | string|null | Si es un reemplazo, a qué pedido reemplaza. |
replaced_by_order_id | string|null | Si fue reemplazado, por cuál. |
support_ticket_id | string|null | Ticket de soporte asociado. |
user_flag | boolean | El tilde de "cuenta usada" que marcás en el portal. |
user_note | string | La nota que le pusiste al pedido en el portal. |
reseller | object | { user_id, name } — cuál de tus cuentas hizo el pedido. Relevante si tu key incluye sub-revendedores. |
Esta sección es la que evita que tu conciliación dé mal. Son tres cosas contraintuitivas:
PENDING ya está pagado. No significa
"pendiente de pago": significa pendiente de entrega. El importe ya se descontó de tu
saldo cuando hiciste el pedido. Si tu sistema lo trata como no cobrado, tu caja no va a cerrar.
order_source: "replacement" (y
replacement_for_order_id apuntando al original) con importe cero.
Es una entrega real, pero no es una venta: si la contás como tal, te queda una
venta fantasma. Lo mismo con order_source: "sin_cargo", que son
regalos o compensaciones.
status: "REFUNDED" y su
refunded_at. Es a propósito. Si lo filtráramos, tu
fila quedaría en "entregado" para siempre y tu ganancia quedaría inflada sin que nada te avise.
Cuando veas un pedido pasar a REFUNDED, descontalo.
| Si… | Es… | Para tus números |
|---|---|---|
status = "REFUNDED" | Devolución | No es una venta. Restala. |
order_source = "replacement" | Reemplazo | Entrega real en $0. No es venta nueva. |
order_source = "sin_cargo" | Regalo / compensación | $0, sin original detrás. |
| ninguna de las anteriores | Venta normal | Contala. |
Esta es la sección importante. Hay dos modos y usar el equivocado hace que tu base mienta.
order | Ordena por | Cuándo usarlo |
|---|---|---|
created | Fecha del pedido | Una sola vez, al principio: la carga inicial de tu histórico. |
updated (default) | Último cambio | Siempre después: es el que corre tu cron. |
created_at. La fecha de
creación de un pedido nunca cambia. Si un pedido pasa de
PENDING a FULFILLED, o se devuelve tres
días después, su created_at sigue siendo el mismo — así que vas a
verlo una única vez, en su primer estado, y no te vas a enterar de nada más.
Con order=updated el pedido vuelve a aparecer cada vez que cambia.
order=created y andá
siguiendo next_cursor hasta que
has_more sea false. Descartá ese
cursor cuando termines.order=updated sin
cursor. Guardá el next_cursor que te devuelve.has_more: false, y guarda el último
next_cursor.// El cron, en pseudocodigo
let cursor = leerCursorGuardado() // null la primera vez
do {
const url = new URL('https://mayoristas.epicodes.com.ar/partner-api.php/orders')
url.searchParams.set('order', 'updated')
url.searchParams.set('limit', '200')
if (cursor) url.searchParams.set('cursor', cursor)
const r = await fetch(url, { headers: { 'X-Api-Key': KEY } })
const d = await r.json()
if (!r.ok) throw new Error(d.error)
for (const p of d.orders) upsertPorSaleNumber(p) // ← UPSERT, no INSERT
cursor = d.next_cursor
guardarCursor(cursor)
} while (d.has_more)
updated_at se mueve por cuestiones internas nuestras sin que cambie
ningún campo que vos veas — por ejemplo cuando cerramos un período de facturación. Recibir un
pedido "otra vez igual" es normal y no significa nada raro.
| Parámetro | Con qué modo | Qué hace |
|---|---|---|
updated_since | updated | Arranca desde esa fecha en vez de desde el principio. |
created_from / created_to | created | Acota el rango del backfill. |
status | los dos | Filtra por estado, separados por coma. Sin este parámetro vienen los tres, que es lo que querés. |
Una fecha YYYY-MM-DD se interpreta como
día argentino completo. Si mandás un ISO 8601 con zona, se respeta tal cual.
Cuando mandás cursor, estos atajos se ignoran: el cursor ya dice
desde dónde seguir.
Por cursor, no por página numerada: así no se te escapa ni se te duplica nada aunque entren pedidos nuevos mientras estás paginando.
limit: entre 1 y 200. Por defecto 100.has_more sea true.next_cursor es opaco. Guardalo como string, no lo parsees.Cada key tiene un alcance fijo, que ves en /ping:
| Alcance | Qué incluye |
|---|---|
propios | Solo los pedidos hechos con tu cuenta. |
propios_y_subs | Los tuyos más los de tus sub-revendedores. El campo reseller de cada pedido te dice de cuál. |
order=created y seguí.
60 requests por minuto por key. Para dimensionar: traer un histórico de ~1.400 pedidos a 200 por página son 7 requests. Un sync incremental que no encuentra novedades es una.
Sincronizar cada 5 o 10 minutos es más que suficiente y no toca el límite ni de cerca.
Si te pasás recibís 429 con el header
Retry-After en segundos. Respetalo y reintentá.
| Status | error | Qué pasó |
|---|---|---|
| 401 | missing_api_key | Falta el header X-Api-Key. |
| 403 | invalid_api_key | La key no existe o está mal copiada. |
| 403 | key_revoked | La key fue dada de baja. Pedinos una nueva. |
| 400 | invalid_cursor | El cursor está corrupto. Volvé a empezar sin cursor. |
| 400 | invalid_param | Algún parámetro está mal. El campo message dice cuál. |
| 404 | order_not_found | Ese pedido no existe o no es tuyo. |
| 429 | rate_limited | Pasaste el límite. Mirá Retry-After. |
{
"ok": false,
"error": "invalid_cursor",
"message": "El cursor no es valido. Volve a empezar sin cursor."
}
/ping./v2 y
/v1 sigue andando, con aviso previo.