Shopify

Apps de Shopify en PHP y Python: los nuevos paquetes oficiales paso a paso

Shopify publicó paquetes oficiales para crear apps en PHP y Python, y dejó obsoletas las librerías anteriores. Te explico qué incluyen, cómo configurarlos con Shopify CLI y cómo verificar un webhook y consultar la Admin API sin reinventar la seguridad.

  • Javi Mata
  • 9 min de lectura
Tabla de contenido

Si alguna vez hiciste una app de Shopify en PHP, seguramente usaste shopify-api-php o armaste a mano la verificación de webhooks con HMAC. Eso cambió: el 26 de agosto de 2026 Shopify publicó paquetes oficiales y de código abierto para crear apps en PHP y Python, y después los incluyó entre las novedades para desarrolladores de la Fall ‘26 Edition. Ya están en la versión 1.0.

En este artículo te explico qué incluyen, qué pasa con las librerías anteriores y cómo dar los primeros pasos. Todo está basado en el changelog oficial y en los README de los paquetes, consultados el 30 de octubre de 2026 (versión 1.0.2 en Packagist y en PyPI).

Qué son

Son dos paquetes con la misma API, uno por lenguaje:

composer require shopify/shopify-app-php
pip install shopifyapp

No son un framework: ofrecen primitivas independientes que combinas como quieras, y funcionan con Laravel, Symfony, CodeIgniter o PHP sin framework, y con Django, Flask o FastAPI en Python. Según el changelog, cubren tres cosas:

  • Verificación de peticiones: webhooks, App Home y App Bridge, app proxy y extensiones de Checkout, POS, Admin, Customer Account y Flow.
  • Intercambio de tokens: token exchange, client credentials y renovación de tokens obtenidos por intercambio.
  • Un cliente para la Admin GraphQL API con reintentos automáticos.

Requisitos según cada README: PHP 8.2 o superior con Composer, o Python 3.8 o superior con pip. Y para desarrollar, una cuenta de Partner, una tienda de desarrollo y Shopify CLI.

Un detalle interesante: los dos paquetes implementan el mismo contrato y pasan la misma batería de pruebas, así que una mejora en uno beneficia al otro.

¿Y las librerías anteriores?

shopify-api-php y shopify_python_api quedan obsoletas: siguen funcionando, pero ya no recibirán funciones nuevas ni correcciones de seguridad. En Packagist, shopify-api-php aparece como abandonada, y en PyPI, shopify_python_api como inactiva.

No hay fecha de retiro ni migración forzada. Los paquetes nuevos están pensados para adoptarse poco a poco, una ruta a la vez, siguiendo las notas de migración de cada README. Como la librería anterior ya no recibe parches de seguridad, mi recomendación es no dejar esa migración indefinidamente.

Qué funciones trae

Los nombres son equivalentes en los dos lenguajes (camelCase en PHP, snake_case en Python):

Para quéPHPPython
WebhooksverifyWebhookReqverify_webhook_req
App Home (app embebida con App Bridge)verifyAppHomeReqverify_app_home_req
App proxyverifyAppProxyReqverify_app_proxy_req
Extensiones de Admin, POS, Checkout y Customer AccountverifyAdminUIExtReq, verifyPosUIExtReq, verifyCheckoutUIExtReq, verifyCustomerAccountUIExtReqverify_admin_ui_ext_req, verify_pos_ui_ext_req, verify_checkout_ui_ext_req, verify_customer_account_ui_ext_req
Acciones de FlowverifyFlowActionReqverify_flow_action_req
Token exchangeexchangeUsingTokenExchangeexchange_using_token_exchange
Client credentialsexchangeUsingClientCredentialsexchange_using_client_credentials
Renovar un tokenrefreshTokenExchangedAccessTokenrefresh_token_exchanged_access_token
Admin GraphQLadminGraphQLRequestadmin_graphql_request

Además hay helpers para App Home: appHomePatchIdToken (la página que renueva un ID token caducado), appHomeParentRedirect (redirigir fuera del iframe del administrador) y appHomeRedirect (redirigir dentro de tu app).

No hay plantilla: armas tu proyecto

No hay plantilla de app para PHP ni para Python. El changelog propone estos pasos:

  1. Instalar el paquete de tu lenguaje.
  2. Crear el proyecto con tu framework o stack preferido.
  3. Combinar las primitivas que necesites, por ejemplo verificar una petición y luego intercambiar el token.
  4. Confirmar en una tienda de desarrollo que las peticiones se verifican y que el intercambio devuelve un token de acceso.

El README explica cómo conectar tu proyecto con Shopify CLI. En la carpeta del proyecto creas un shopify.app.toml (que se sobrescribe al ejecutar shopify app init --reset). Este es el del README:

client_id = ""
name = ""
application_url = ""
embedded = true

[access_scopes]
scopes = "write_products"

[webhooks]
api_version = "2025-01"

Necesitas también un package.json mínimo:

{
  "name": "my-php-app"
}

Y un shopify.web.toml para que la CLI sepa cómo levantar tu app en desarrollo:

name = "My PHP App"
roles = ["frontend", "backend"]
webhooks_path = "/webhooks/app/uninstalled"

[commands]
dev = "[COMMAND]"

Sustituye [COMMAND] por el comando de tu framework; para Laravel, el README indica php artisan serve. Después ejecutas:

shopify app dev --reset

El flag --reset solo se usa la primera vez.

El “camino verde”: el mismo flujo en cada petición

El README resume lo que hace casi cualquier app en cada petición de Shopify:

  1. Convertir la petición de tu framework al formato del paquete.
  2. Verificarla con la función verify... que corresponda.
  3. Si ok es false, devolver la response que te da el paquete.
  4. Usar $result->shop para buscar o guardar el token de acceso de esa tienda.
  5. Intercambiar o renovar el token cuando haga falta.
  6. Hacer las llamadas a Admin GraphQL.

Y una regla que repite en todo el documento: si el paquete ya te da un valor o una respuesta, úsalo tal cual. No lo parsees, no lo armes tú ni lo vuelvas a verificar.

1. Inicializar

Shopify CLI te da las variables SHOPIFY_API_KEY y SHOPIFY_API_SECRET:

<?php

use Shopify\App\ShopifyApp;

$shopify = new ShopifyApp(
    clientId: getenv('SHOPIFY_API_KEY'),
    clientSecret: getenv('SHOPIFY_API_SECRET'),
);

Si vas a rotar el client secret, existe el parámetro opcional oldClientSecret: mientras dure la rotación se aceptan peticiones firmadas con cualquiera de los dos, sin cortes.

2. Convertir la petición y la respuesta

Para ser independiente del framework, el paquete trabaja con su propio formato de petición. Este es el ejemplo para Laravel del README:

// Laravel passes the request to controllers and middleware
function requestToShopifyReq($request)
{
    return [
        'method' => $request->method(),
        'headers' => $request->headers->all(),
        'url' => $request->fullUrl(),
        'body' => $request->getContent(),
    ];
}

Lo importante: pasa la petición sin modificar, con todos los encabezados, la URL completa con su query string y el cuerpo original. La verificación depende de los bytes exactos que mandó Shopify.

Y el camino inverso, del resultado del paquete a una respuesta de Laravel, registrando de paso el log que trae cada resultado:

use Illuminate\Support\Facades\Log;
use Illuminate\Http\Response;

function shopifyResultToResponse($result)
{
    $log = $result->log;
    Log::info("{$log->code} - {$log->detail}");

    $resp = $result->response;
    return response($resp->body, $resp->status)
        ->withHeaders((array) $resp->headers);
}

3. Verificar un webhook

Con esas dos funciones, verificar un webhook queda así (ejemplo del README):

function webhookHandler($request)
{
    $shopify = getShopifyApp();
    $req = requestToShopifyReq($request);

    $result = $shopify->verifyWebhookReq($req);
    if (!$result->ok) {
        return shopifyResultToResponse($result);
    }

    // Your database logic here
    $accessToken = getAccessToken($result->shop, 'offline');
}

getShopifyApp(), getAccessToken() y el resto de la lógica de base de datos los escribes tú: el paquete no guarda tokens. Lo que sí define es la forma del token (shop, accessMode, token, scope, refreshToken, expires, refreshTokenExpires y user).

4. Consultar la Admin API

En un proceso en segundo plano, como procesar un webhook o una tarea programada, pasas null en invalidTokenResponse, porque no hay una petición en curso que Shopify pueda reintentar:

function processJob($shop)
{
    $shopify = getShopifyApp();
    // Your database logic here
    $accessToken = getAccessToken($shop, 'offline');

    $graphqlResult = $shopify->adminGraphQLRequest(
        '
        {
            shop {
                id
            }
        }
        ',
        shop: $shop,
        accessToken: $accessToken->token,
        apiVersion: '2025-01',
        invalidTokenResponse: null,
    );

    if (!$graphqlResult->ok) {
        return;
    }

    $shopId = $graphqlResult->data['shop']['id'];
}

adminGraphQLRequest acepta además variables, headers y maxRetries (2 por defecto). El resultado trae ok, data, extensions (por ejemplo, el costo de la consulta), log y httpLogs. Si algo falla, decide según $graphqlResult->log->code, no según el texto del error: es lo que pide el README.

Los ejemplos del README usan apiVersion: '2025-01'; en tu app, usa la versión de la Admin API con la que estés trabajando.

Client credentials: integraciones de servidor a servidor

Para procesos internos sin un comerciante delante (automatizaciones, servicios de back office), exchangeUsingClientCredentials obtiene un token solo con el client ID y el client secret:

$result = $shopify->exchangeUsingClientCredentials(shop: $shop);

Según el README, estos tokens caducan a las 24 horas y no traen refresh token: cuando caducan, pides otro de la misma forma. Y no se usan para autenticar peticiones de App Home o de extensiones, que traen su propio ID token para intercambiar.

Lo que el README te pide no hacer

Esta tabla resume los errores que evita el paquete. Vale la pena leerla aunque no lo uses:

No hagas estoHaz esto
Sacar la tienda del ID token o de la peticiónUsar $result->shop
Crear tu propia página para renovar el tokenUsar appHomePatchIdToken
Renderizar App Home sin los encabezados de la respuestaCopiar $result->response->headers (incluyen la protección del iframe, como frame-ancestors)
Armar tu propia respuesta cuando la verificación fallaDevolver $result->response tal cual
Modificar encabezados, query string o cuerpo antes de verificarPasar la petición sin cambios
Poner el ID token en la URL para mantener la sesión al navegarConfigurar la ruta de renovación y pasar appHomePatchIdTokenPath en cada verificación

En Python es lo mismo

El paquete de Python expone las mismas funciones en snake_case. La inicialización:

import os
from shopify_app import ShopifyApp

shopify = ShopifyApp(
    client_id=os.getenv("SHOPIFY_API_KEY"),
    client_secret=os.getenv("SHOPIFY_API_SECRET"),
)

Y el webhook:

def webhook_handler(request):
    req = request_to_shopify_req(request)

    result = shopify.verify_webhook_req(req)
    if not result.ok:
        return shopify_result_to_response(result)

    # Your database logic here
    access_token = get_access_token(shop=result.shop, mode="offline")

Para frameworks asíncronos como FastAPI, cada llamada de red tiene su versión con sufijo _async, por ejemplo admin_graphql_request_async, para no bloquear el event loop.

¿Cuándo usarlos?

El changelog es claro: para la mayoría de las apps nuevas, React Router sigue siendo el camino recomendado, y nada cambia si ya usas las librerías de Node.js o Ruby. Mi recomendación:

  • Usa estos paquetes si tu equipo o tu infraestructura ya son PHP o Python, por ejemplo un Laravel que ya sincroniza inventario con un ERP o un backend en Django.
  • Migra las apps que usan shopify-api-php o shopify_python_api ruta por ruta, empezando por los webhooks, que son lo más sencillo de verificar.
  • Dale el README a tu agente de IA. El changelog sugiere usarlo como contexto para una herramienta de programación con IA al montar el proyecto. Si usas Claude Code con Shopify, lo expliqué en Shopify AI Toolkit con Claude Code.

Conclusión

Por fin hay una forma oficial y mantenida de crear apps de Shopify en PHP y Python. No te da una app hecha, pero sí las piezas que más se suelen hacer mal a mano: verificar peticiones, manejar tokens y consultar la Admin API con reintentos. Si tienes código con la librería anterior, ya no recibirá parches de seguridad: es buen momento para planear la migración.

Fuentes