codwelt / sdk-envato
SDK PHP de Codwelt para la API oficial de Envato Market.
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.8
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
- illuminate/support: Permite usar el service provider y la Facade en Laravel (^10.0 || ^11.0 || ^12.0 || ^13.0).
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-19 02:08:47 UTC
README
SDK Envato para PHP
SDK de Codwelt para la
API oficial de Envato: catálogo, usuarios, ventas,
compras, descargas, verificación de códigos de compra y OAuth.
Parte de Toolsbox,
herramientas digitales gratuitas y de código abierto.
📘 ¿Qué es este SDK?
SDK Envato (codwelt/sdk-envato) es una librería PHP para consumir la API oficial de Envato, documentada en build.envato.com/api. En vez de escribir peticiones HTTP a mano contra https://api.envato.com, instalas el paquete con Composer y llamas a métodos PHP con nombre claro.
| La API de Envato ofrece… | …y con el SDK lo haces así |
|---|---|
| Consultar el catálogo de Envato Market: artículos, búsquedas, categorías, populares y precios | $envato->catalog()->searchItems([...]) |
| Verificar un código de compra de tu tema o plugin | $envato->verifyPurchaseCode($code) |
| Ver tus ventas como autor y tus extractos | $envato->author()->sales() · $envato->account()->statement() |
| Listar y descargar compras como comprador | $envato->buyer()->listPurchases() · $envato->buyer()->download([...]) |
| Consultar perfiles, colecciones y badges de usuarios | $envato->user()->details('usuario') |
| Conectar cuentas de terceros mediante OAuth | $envato->oauth()->getAuthorizationUrl() |
Para quién es: autores de ThemeForest o CodeCanyon que validan licencias y soporte, plataformas que muestran o sincronizan artículos de Envato, y aplicaciones que permiten a sus usuarios conectar su cuenta de Envato.
Qué necesitas: un token personal creado en build.envato.com/create-token o una app OAuth registrada en build.envato.com/my-apps. El SDK no sustituye esa documentación: la sigue endpoint por endpoint, y cada método de la referencia de recursos indica la ruta de la API que llama.
Note
La API de build.envato.com da acceso a Envato Market (ThemeForest, CodeCanyon, VideoHive, AudioJungle, GraphicRiver, PhotoDune y 3DOcean). Envato Elements no tiene API pública, así que este SDK no incluye endpoints de Elements.
Contenido
✨ Qué incluye
🚀 Instalación
Requiere PHP ^8.1 y Composer. Usa Guzzle ^7.8; Laravel es opcional y no se instala como dependencia del SDK.
composer require codwelt/sdk-envato
Tip
El comando funcionará cuando el paquete esté publicado en Packagist. Mientras tanto, en desarrollo local, declara este repositorio como repositorio Composer de tipo path en tu aplicación.
🔑 Obtener un token personal
flowchart LR
A["Inicia sesión en<br/>build.envato.com"] --> B["Abre<br/>Create a token"]
B --> C["Elige los permisos<br/>que necesitas"]
C --> D["Crea el token"]
D --> E["Guárdalo en<br/>ENVATO_TOKEN"]
Loading
- Inicia sesión en Envato y abre Create a token.
- Nombra el token y concede los permisos necesarios para tu integración.
- Acepta los términos, crea el token y guárdalo en
ENVATO_TOKENen tu entorno de ejecución.
Los permisos dependen del recurso: consulta de items y búsquedas, colecciones privadas, verificación de ventas del autor, historial de ventas, compras y descargas del comprador, cuenta, usuario, correo y extractos. Revisa el permiso de cada operación en la documentación oficial.
- Un código de compra solo se puede verificar como venta de un artículo del autor autenticado.
- Los endpoints de comprador requieren el token del comprador.
- Un token válido sin el permiso requerido produce un 403.
Warning
No incluyas tokens ni secretos en el código fuente. El SDK no carga archivos .env: lee los valores que le pases. En Laravel se usa su sistema de configuración habitual.
⚡ Inicio rápido
require 'vendor/autoload.php'; use Codwelt\Envato\EnvatoClient; $token = getenv('ENVATO_TOKEN') ?: throw new RuntimeException('Falta ENVATO_TOKEN'); $envato = EnvatoClient::make($token); $item = $envato->catalog()->item(123); $results = $envato->catalog()->searchItems([ 'term' => 'portfolio', 'site' => 'themeforest', 'page' => 1, ]); $sale = $envato->verifyPurchaseCode('01234567-89ab-cdef-0123-456789abcdef'); // array de venta, o null si la API responde 404.
Todas las respuestas son arrays con la estructura original de Envato. No hay paginación automática: solicita cada página explícitamente. Los recursos se crean bajo demanda y cada acceso devuelve la misma instancia.
🧭 Arquitectura
flowchart LR
App["Tu aplicación<br/>(PHP o Laravel)"] --> Client["EnvatoClient"]
Client --> Catalog["catalog()"]
Client --> User["user()"]
Client --> Author["author()"]
Client --> Buyer["buyer()"]
Client --> Account["account()"]
Client --> Stats["stats()"]
Client --> OAuth["oauth()"]
Catalog & User & Author & Buyer & Account & Stats & OAuth --> Http["HttpClient<br/>Bearer · reintentos · errores tipados"]
Http --> Guzzle["Guzzle 7"]
Guzzle --> API[("api.envato.com")]
Loading
| Pieza | Responsabilidad |
|---|---|
EnvatoClient |
Punto de entrada. Expone los recursos, verifyPurchaseCode() y withToken(). |
EnvatoConfig |
Token, credenciales OAuth, URL base, User-Agent, timeout y reintentos. |
HttpClient |
Añade el Bearer y el User-Agent, decodifica JSON, reintenta y convierte errores en excepciones. |
Api\* |
Un recurso por grupo de la API, con validación de argumentos antes de llamar. |
Auth\OAuth y Auth\AccessToken |
Flujo OAuth y token con expiración absoluta. |
Laravel\* |
ServiceProvider, configuración publicable y Facade. |
⚙️ Configuración e inyección de Guzzle
use Codwelt\Envato\Config\EnvatoConfig; use Codwelt\Envato\EnvatoClient; use GuzzleHttp\Client; $envato = new EnvatoClient( new EnvatoConfig( token: $token, baseUri: 'https://api.envato.com', userAgent: 'mi-aplicacion/1.0', timeout: 30, maxRetries: 3, ), new Client(), // ClientInterface: admite tu handler o configuración de proxy. static function (float $seconds): void { usleep((int) round($seconds * 1_000_000)); }, ); $anotherUser = $envato->withToken($anotherAccessToken);
| Opción | Valor por defecto |
|---|---|
baseUri |
https://api.envato.com |
timeout |
30 segundos |
maxRetries |
3 |
userAgent |
codwelt-sdk-envato/1.0 (+https://codwelt.com) |
withToken()devuelve un clon con recursos nuevos. Conserva el transporte, las opciones y el sleeper; el cliente original mantiene su token.- El sleeper recibe segundos.
make($token, $options)acepta los nombres camelCase del constructor deEnvatoConfig, másclientysleeper.- Los encabezados, el timeout, el tratamiento de errores y las redirecciones los controla el SDK. No se siguen redirecciones HTTP.
🅻 Laravel
El paquete declara autodiscovery para el provider y el alias Envato. En una aplicación Laravel con illuminate/support, publica la configuración:
php artisan vendor:publish --tag=envato-config
Configura tu .env:
ENVATO_TOKEN= ENVATO_CLIENT_ID= ENVATO_CLIENT_SECRET= ENVATO_REDIRECT_URI=https://tu-dominio.com/envato/callback ENVATO_USER_AGENT="mi-aplicacion/1.0" ENVATO_TIMEOUT=30 ENVATO_MAX_RETRIES=3
use Codwelt\Envato\Laravel\Facades\Envato; use Codwelt\Envato\EnvatoClient; $item = Envato::catalog()->item(123); $client = app(EnvatoClient::class); // también app('envato') $oauth = app(\Codwelt\Envato\Auth\OAuth::class);
El provider combina config/envato.php, registra EnvatoClient y OAuth como singletons y publica el archivo con el tag envato-config. Si deshabilitas autodiscovery, registra Codwelt\Envato\Laravel\EnvatoServiceProvider manualmente. Para atender a distintos usuarios OAuth, usa withToken() en cada solicitud.
🔐 OAuth
Registra tu aplicación en My Apps con los permisos requeridos. La redirectUri debe usar HTTPS y coincidir exactamente con la Confirmation URL configurada en Envato.
sequenceDiagram
autonumber
actor U as Usuario
participant A as Tu aplicación
participant E as api.envato.com
A->>U: Redirige a getAuthorizationUrl()
U->>E: Inicia sesión y autoriza la app
E-->>A: Callback con ?code= (un solo uso)
A->>E: POST /token · exchangeCode($code)
E-->>A: access_token + refresh_token (1 hora)
A->>E: GET /v1/... con Bearer (withToken)
Note over A,E: Cuando caduca
A->>E: POST /token · refreshToken($refresh)
E-->>A: Nuevo access_token
Loading
$envato = EnvatoClient::make('', [ 'clientId' => getenv('ENVATO_CLIENT_ID'), 'clientSecret' => getenv('ENVATO_CLIENT_SECRET'), 'redirectUri' => getenv('ENVATO_REDIRECT_URI'), ]); $oauth = $envato->oauth(); $url = $oauth->getAuthorizationUrl(); // Redirige al usuario a $url; Envato devuelve un código de un solo uso al callback. $token = $oauth->exchangeCode($codeFromCallback); $client = $envato->withToken($token->accessToken); if ($token->isExpired()) { $token = $oauth->refreshToken($token->refreshToken); $client = $envato->withToken($token->accessToken); }
getAuthorizationUrl(?string $state = null)permite añadir un estado que tu callback debe comprobar. El ejemplo oauth_flow.php muestra autorización, callback, sesión y renovación.- Los tokens se piden únicamente desde el servidor:
POST /token, formularioapplication/x-www-form-urlencoded, sin Bearer. Estas peticiones no se reintentan, para no reutilizar un código de un solo uso. AccessTokenexpone las propiedades readonlyaccessToken,refreshToken(nullable),expiresAt(timestamp Unix) ytokenType.fromArray($data, ?int $now = null)convierteexpires_inen una expiración absoluta.isExpired(?int $now = null)considera expirado el instante exacto de vencimiento.- Si una renovación no devuelve
refresh_token, se conserva el anterior. La renovación no es automática: guarda los tokens de forma segura y persiste el nuevo después de renovar.
✅ Verificar códigos de compra
El caso de uso más habitual: comprobar que un cliente compró tu tema o plugin antes de darle soporte o activar una licencia.
flowchart TD
In["verifyPurchaseCode($code)"] --> Trim["Recorta espacios"]
Trim --> Fmt{"¿Formato UUID?"}
Fmt -- No --> VE["ValidationException<br/>(sin llamada HTTP)"]
Fmt -- Sí --> Call["GET /v3/market/author/sale?code="]
Call --> R{"Respuesta"}
R -- 200 --> Sale["array con la venta"]
R -- 404 --> Null["null"]
R -- Otro error --> Ex["Excepción tipada"]
Loading
$sale = $envato->verifyPurchaseCode($request->input('purchase_code')); if ($sale === null) { // El código no corresponde a una venta de tus artículos. } $itemName = $sale['item']['name']; $buyer = $sale['buyer']; $supportedUntil = $sale['supported_until'];
📚 Referencia de recursos
Todos los métodos realizan GET. Las rutas son relativas a https://api.envato.com; los parámetros {...} son segmentos codificados con rawurlencode y el resto viaja en la query. Los ids y las páginas deben ser positivos y los strings no vacíos; download() exige item_id o purchase_code. Los argumentos inválidos lanzan InvalidArgumentException antes de hacer la petición.
🛍️ catalog(): catálogo de Envato Market
| Método | Ruta | Query |
|---|---|---|
collection(int $id, ?int $page = null) |
/v3/market/catalog/collection |
id=id, page=page |
item(int $id) |
/v3/market/catalog/item |
id=id |
itemVersion(int $id) |
/v3/market/catalog/item-version |
id=id |
searchItems(array $params = []) |
/v1/discovery/search/search/item |
array libre de parámetros documentados |
searchComments(int $itemId, array $params = []) |
/v1/discovery/search/search/comment |
item_id=itemId + params |
similarItems(int $itemId, array $params = []) |
/v1/discovery/search/search/more_like_this |
item_id=itemId + params |
popular(string $site) |
/v1/market/popular:{site}.json |
— |
categories(string $site) |
/v1/market/categories:{site}.json |
— |
itemPrices(int $itemId) |
/v1/market/item-prices:{itemId}.json |
— |
newItems(string $site, string $category) |
/v1/market/new-files:{site},{category}.json |
— |
featured(string $site) |
/v1/market/features:{site}.json |
— |
randomNewItems(string $site) |
/v1/market/random-new-files:{site}.json |
— |
👤 user(): datos de usuarios
| Método | Ruta | Query |
|---|---|---|
collections() |
/v3/market/user/collections |
— |
privateCollection(int $id) |
/v3/market/user/collection |
id=id |
details(string $username) |
/v1/market/user:{username}.json |
— |
badges(string $username) |
/v1/market/user-badges:{username}.json |
— |
itemsBySite(string $username) |
/v1/market/user-items-by-site:{username}.json |
— |
newItems(string $username, string $site) |
/v1/market/new-files-from-user:{username},{site}.json |
— |
💰 author(): ventas del autor
| Método | Ruta | Query |
|---|---|---|
sales(?int $page = null) |
/v3/market/author/sales |
page=page |
saleByCode(string $code) |
/v3/market/author/sale |
code=code |
🛒 buyer(): compras y descargas
| Método | Ruta | Query |
|---|---|---|
listPurchases(array $params = []) |
/v3/market/buyer/list-purchases |
array libre de parámetros documentados |
purchases(?int $page = null) |
/v3/market/buyer/purchases |
page=page |
purchaseByCode(string $code) |
/v3/market/buyer/purchase |
code=code |
download(array $params) |
/v3/market/buyer/download |
array libre de parámetros documentados |
🪪 account(): cuenta privada
| Método | Ruta | Query |
|---|---|---|
details() |
/v1/market/private/user/account.json |
— |
username() |
/v1/market/private/user/username.json |
— |
email() |
/v1/market/private/user/email.json |
— |
salesByMonth() |
/v1/market/private/user/earnings-and-sales-by-month.json |
— |
statement(array $params = []) |
/v3/market/user/statement |
array libre de parámetros documentados |
📊 stats(): estadísticas de Envato Market
| Método | Ruta | Query |
|---|---|---|
totalUsers() |
/v1/market/total-users.json |
— |
totalItems() |
/v1/market/total-items.json |
— |
filesInCategory(string $site) |
/v1/market/number-of-files:{site}.json |
— |
Los arrays $params se transmiten sin lista restrictiva de claves, para admitir los filtros oficiales:
| Método | Parámetros habituales |
|---|---|
searchItems() |
term, site, category, price_min, price_max, rating_min, sales, tags, username, date, page, page_size, sort_by, sort_direction y otros de la documentación |
searchComments() |
term, page, page_size, sort_by. El itemId explícito prevalece sobre params['item_id'] |
similarItems() |
page, page_size |
listPurchases() |
filter_by (wordpress-themes o wordpress-plugins), page, include_all_item_details |
download() |
item_id o purchase_code, opcionalmente shorten_url |
statement() |
page, from_date, to_date, type, site |
Verificación de la documentación
El 17 de octubre de 2026 se cotejaron las 32 rutas con el API Method Listing y la especificación oficial que usa esa página. Todas las rutas y sus verbos GET quedaron confirmados, igual que OAuth (/authorization, /token) y Retry-After en segundos.
El parámetro state de OAuth es una extensión del SDK: la documentación pública no confirma que Envato lo devuelva en el callback. Compruébalo con tu aplicación antes de desplegar el ejemplo de sesión. Durante la verificación no se hicieron llamadas autenticadas. Detalles en el informe de endpoints.
🚦 Errores y límites de solicitudes
use Codwelt\Envato\Exceptions\RateLimitException; use Codwelt\Envato\Exceptions\EnvatoException; try { $items = $envato->catalog()->searchItems(['term' => 'shop']); } catch (RateLimitException $exception) { $waitSeconds = $exception->getRetryAfter(); // int, o null si falta o no es válido } catch (EnvatoException $exception) { $status = $exception->getStatusCode(); $body = $exception->getBody(); $message = $exception->getMessage(); }
| HTTP | Excepción |
|---|---|
| 🟠 400 / 422 | ValidationException |
| 🔴 401 | AuthenticationException |
| 🔴 403 | ForbiddenException |
| ⚪ 404 | NotFoundException |
| 🟡 429 | RateLimitException |
| 🟣 5xx | ServerException |
| ⚫ Otro error HTTP, JSON inválido o fallo de transporte | ApiException |
Todas heredan de EnvatoException y exponen statusCode, body, getStatusCode() y getBody(). El mensaje usa la primera clave escalar disponible entre error_description, error, message y reason; si no hay ninguna, incluye el estado HTTP. Un error local de formato de código de compra tiene status 0, igual que un fallo de transporte (con la causa en getPrevious()).
flowchart LR
Req["Petición GET"] --> Resp{"Respuesta"}
Resp -- 2xx --> OK["Array JSON"]
Resp -- "429 / 5xx" --> Left{"¿Quedan<br/>reintentos?"}
Left -- Sí --> Wait["Espera max(Retry-After,<br/>2^intento + jitter)"]
Wait --> Req
Left -- No --> Err["Excepción tipada"]
Resp -- "4xx" --> Err
Loading
Envato impone límites dinámicos, sin una cuota fija. El SDK reintenta solo los 429 y 5xx de peticiones GET, hasta maxRetries veces además del primer intento. La espera es exponencial (2^intento segundos, con el exponente limitado a 20) más un jitter de 0 a 1 segundo, y nunca menor que el Retry-After recibido. El sleeper por defecto espera de forma síncrona; puedes inyectar otro. Los errores de transporte no se reintentan.
🧪 Ejemplos y pruebas
| Ejemplo | Qué muestra |
|---|---|
| verify_purchase_code.php | Verificar un código de compra desde la línea de comandos |
| search_items.php | Buscar artículos en el catálogo |
| author_sales.php | Listar las ventas del autor por página |
| oauth_flow.php | Flujo OAuth completo: autorización, callback y renovación |
composer install
composer validate --strict
composer test
php examples/search_items.php portfolio
php examples/verify_purchase_code.php 01234567-89ab-cdef-0123-456789abcdef
php examples/author_sales.php 1
Los ejemplos de línea de comandos leen ENVATO_TOKEN del entorno. El ejemplo OAuth se sirve como página HTTPS con ENVATO_CLIENT_ID, ENVATO_CLIENT_SECRET y ENVATO_REDIRECT_URI.
Las pruebas usan PHPUnit ^10.5 con MockHandler y Middleware::history: rutas y query de todos los métodos, Bearer, mapeo de errores, reintentos con sleeper falso, OAuth, clonación y códigos de compra. La suite de integración se salta si no hay ENVATO_TOKEN; si lo defines, SmokeTest consulta stats()->totalItems() contra la API real. Para ejecutar solo las pruebas sin red:
vendor/bin/phpunit --testsuite Unit
📦 Publicación en Packagist
- Comprueba
composer validate --strict,composer install,composer testy el lint de PHP. - Sube el repositorio a GitHub.
- Envía su URL a Packagist y activa el webhook de actualización.
- Crea y publica un tag semántico (
v1.0.0) para que Composer lo resuelva como versión estable.
composer.lock y vendor/ se ignoran por ser una librería. tests/, examples/ y docs/ se excluyen del paquete instalado mediante export-ignore.
🧰 Toolsbox por Codwelt
|
|
Herramientas digitales gratuitas. Más tiempo para crear.Este SDK forma parte del ecosistema abierto de Codwelt. Toolsbox reúne 30 herramientas para imágenes, código, SEO y texto: úsalo en línea, instálalo con tu marca o contribuye con tu propia herramienta. 30 herramientas · 6 categorías · 100 % gratis · licencia MIT |
🤝 Licencia y soporte
Licencia MIT. Autor y marca: Codwelt, Bogotá, Colombia y Guayaquil, Ecuador.
| Canal | Contacto |
|---|---|
| ✉️ Correo | comercial@codwelt.com |
| 🌐 Web | codwelt.com |
| 🧰 Proyecto | Toolsbox: herramientas digitales gratuitas |
| 📘 API de Envato | build.envato.com/api |