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ó.
https://mayoristas.epicodes.com.ar/partner-api.php El 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.
⚠️ La key va en el header, nunca en la URL. Un
?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.
Es de solo lectura. No hay ningún endpoint que cree, modifique o cancele
pedidos. Una key filtrada expone tu historial comercial, nunca tu saldo ni tus compras.
Los tres endpoints
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.
2Cómo hacer la request
Empezá por /ping: confirma que la key anda y te muestra qué
cuentas vas a recibir.
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)
}
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.
4Campos de cada pedido
Identificación
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.
⚠️ No uses 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.
Producto
Campo
Tipo
Descripción
title
string
Nombre del juego.
platform
string|null
PS4 o PS5.
license
string|null
Primario o Secundario.
Importes
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.
Fechas (ISO 8601, siempre en UTC)
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).
Relaciones y extras
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.
5Estados y qué significan
Esta sección es la que evita que tu conciliación dé mal. Son tres cosas contraintuitivas:
1. 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.
2. Los reemplazos vienen en $0 y no son ventas nuevas. Si a un pedido tuyo le
dimos una cuenta de reposición, vas a ver un pedido con
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.
3. Las devoluciones te llegan, marcadas. Un pedido devuelto no desaparece de
la API: sigue viniendo con 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.
Cómo clasificar cada renglón
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.
6Sincronización incremental
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.
⚠️ No sincronices por 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.
El flujo completo
Carga inicial. Pedí con order=created y andá
siguiendo next_cursor hasta que
has_more sea false. Descartá ese
cursor cuando termines.
Primer sync. Pedí con order=updated sin
cursor. Guardá el next_cursor que te devuelve.
De ahí en más. Cada corrida arranca con el cursor guardado, pagina hasta
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)
Guardá con UPSERT, no con INSERT. Un pedido te va a llegar varias veces a lo
largo de su vida: cuando se crea, cuando se entrega, si se devuelve. Y a veces
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.
Podés rebobinar sin miedo. Si dudás de tu cursor, guardá uno más viejo y
volvé a correr: la respuesta es idempotente y el UPSERT se encarga. Una práctica sana es
solapar un minuto en cada corrida.
Atajos de arranque
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.
7Paginación
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.
Seguí mientras has_more sea true.
El next_cursor es opaco. Guardalo como string, no lo parsees.
Un cursor inválido devuelve 400, no una lista.
Es a propósito: si te devolviéramos el histórico desde cero ante un cursor roto, un error tuyo
se convertiría en una recarga completa silenciosa en cada corrida.
8El alcance de tu key
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.
⚠️ Si cambia tu alcance, rehacé el backfill. Cuando sumamos un revendedor
nuevo a tu cuenta, o cuando ampliamos el alcance de tu key, sus pedidos viejos tienen
fechas de modificación anteriores a tu cursor actual: no te van a llegar por el sync
incremental. Corré una vez más el modo order=created y seguí.
9Límite de uso
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á.
10Manejo de errores
El código viene en el status HTTP. A diferencia de nuestra API de catálogo
—que siempre responde 200 y pone el código adentro del JSON— acá el status es real. Tu
librería HTTP lo va a interpretar bien sola.
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."
}
11Qué NO devuelve esta API
Las credenciales de las cuentas. Ni el mail, ni la contraseña, ni el
código de verificación. La entrega a tu cliente final se sigue haciendo desde el portal,
como hasta ahora.
Nuestros costos. Vas a ver lo que pagaste vos, que es tu costo. Lo que
nos costó a nosotros no viaja.
Nada de otros mayoristas. Tu key solo alcanza las cuentas que lista
/ping.
Compatibilidad
Podemos agregar campos nuevos en cualquier momento. Tu integración tiene
que ignorar los que no conoce.
Un campo que ya existe no cambia de tipo ni de significado.
Si algún día hace falta un cambio que rompe, sale como /v2 y
/v1 sigue andando, con aviso previo.