Como integrar a API do Mercado Livre
Tags: webdev, braziliandevs, node, typescript
Published: 2024-03-06 21:44:07

Meu nome é Julio e hoje vou mostrar como integrar a API do [Mercado Livre](https://developers.mercadolivre.com.br/pt_br/api-docs-pt-br) com o S3 da AWS, para envio e armazenamento de infoprodutos. Escolhi utiliza-lo, pois é permitido o upload de arquivos de até 100gb manualmente. Você pode me encontrar no [LinkedIn](https://www.linkedin.com/in/fiamon/) ou no [GitHub](https://github.com/fiamon).

## Criando um webhook
Antes de tudo, precisamos criar uma "aplicação", que é como os webhooks do Mercado Livre são chamados. Acesse o link: https://developers.mercadolivre.com.br/devcenter, vincule sua conta e clique em "criar nova aplicação". Na página que aparecer, você pode inserir as informações que preferir; não fará muita diferença para o que vou ensinar. No campo "URIs de redirect", você pode inserir qualquer link acessível. Pode ser o YouTube, seu portfólio ou algo assim. Vou explicar mais adiante para que isso serve.

Quanto aos escopos, aconselho a marcar todos. Eles representam as permissões que seu software terá no Mercado Livre, por meio da sua conta.

Em "Tópicos", você pode selecionar as opções das quais deseja ser notificado. No nosso caso, vamos marcar "Orders_v2".

Agora, a parte mais importante, o local que você configura para receber as notificações. Pode ocorrer um BUG que informa não ser possível inserir a URL desejada. Para resolver, basta abrir a seção "Outros", marcar algum tópico e depois desmarcá-lo. Recomendo utilizar o [ngrok](https://ngrok.com/) para testar os webhooks. Caso tenha interesse, pode ler mais [aqui](https://programmingpercy.tech/blog/webhook-testing-without-the-headache/), foi o mesmo artigo que me ajudou a entender melhor o ngrok.


## Pegando dados para realizar a autenticação
Após ter feito o primeiro passo, você será redirecionado para a página inicial, onde deverá clicar nos três pontinhos e em "editar". Nessa parte, você verá dois campos, um sendo o ID da aplicação (vou usá-lo como 2200211202, para melhorar a explicação) e o outro sendo o secret (vou usá-lo como 10ZEWGtPdD7jxhUdYAAO3lGOQTvm3UJX, para melhorar a explicação). Você vai precisar de ambos. Com as informações em mãos, o próximo passo é autenticar na aplicação.

Aqui está a URL completa para o BRASIL, basta inserir as informações que você obteve:

https://auth.mercadolivre.com.br/authorization?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL


Então, no nosso caso, ficaria assim:

https://auth.mercadolivre.com.br/authorization?response_type=code&client_id=2200211202&redirect_uri=https://www.youtube.com/

REDIRECT_URL: https://www.youtube.com/
CLIENT_ID: 2200211202

Após inserir as informações, basta colar a URL no navegador, autorizar e você será redirecionado. Ao ser redirecionado, você deverá ver na URL algo assim: TG-65d65b17d124c20001f0c119-1694513136. Guarde esse código. Vamos precisar dele no futuro.

## Autenticação e autorização
Agora, uma parte essencial: Autenticação e autorização. Com o código obtido, precisamos trocá-lo por um token. Para fazer isso, você deve colar a seguinte requisição no Postman:

```
curl -X POST \
-H 'accept: application/json' \
-H 'content-type: application/x-www-form-urlencoded' \
'https://api.mercadolibre.com/oauth/token' \
-d 'grant_type=authorization_code' \
-d 'client_id=$APP_ID' \
-d 'client_secret=$SECRET_KEY' \
-d 'code=$SERVER_GENERATED_AUTHORIZATION_CODE' \
-d 'redirect_uri=$REDIRECT_URI' \
-d 'code_verifier=$CODE_VERIFIER'
```

grant_type: authorization_code

APP_ID: será nosso CLIENT_ID (2200211202)

client_secret será nosso secret (10ZEWGtPdD7jxhUdYAAO3lGOQTvm3UJX)

code: Aqui você deverá colocar o código recebido na URL (algo como TG-65d65b17d124c20001f0c119-1694513136)

redirect_uri: https://www.youtube.com/

code_verifier: $CODE_VERIFIER

Para colar uma requisição no Postman, você pode seguir estes passos: Na página inicial, clique em "Import" (fica ao lado do seu workspace), e pressione Ctrl + V.

A resposta deverá ser algo assim:

```
{
    "access_token": "APP_USR-123456-090515-8cc4448aac10d5105474e1351-1234567",
    "token_type": "bearer",
    "expires_in": 21600,
    "scope": "offline_access read write",
    "user_id": 1234567,
    "refresh_token": "TG-5b9032b4e23464aed1f959f-1234567"
}

```
Após fazer essa troca, devemos obter um refresh token. Esse token serve para ser trocado por outro access token após 6 horas do primeiro uso. Caso você utilize esse refresh token para qualquer ação no Mercado Livre, será necessário inserir o link de autenticação no navegador para obter outro código. Cada ação na plataforma deverá ser feita usando o access_token.

```
curl -X POST \
-H 'accept: application/json' \
-H 'content-type: application/x-www-form-urlencoded' \
'https://api.mercadolibre.com/oauth/token' \
-d 'grant_type=refresh_token' \
-d 'client_id=$APP_ID' \
-d 'client_secret=$SECRET_KEY' \
-d 'refresh_token=$REFRESH_TOKEN'

```
grant_type: refresh_token

client_id: 2200211202

client_secret: 10ZEWGtPdD7jxhUdYAAO3lGOQTvm3UJX

refresh_token: TG-5b9032b4e23464aed1f959f-1234567

A resposta deverá ser algo assim:

```
{
    "access_token": "APP_USR-5387223166827464-090515-b0ad156bce700509ef81b273466faa15-8035443",
    "token_type": "bearer",
    "expires_in": 21600,
    "scope": "offline_access read write",
    "user_id": 8035443,
    "refresh_token": "TG-5b9032b4e4b0714aed1f959f-8035443"
}
```


## Testando
Como mencionado anteriormente, com esse access_token você poderá fazer qualquer requisição/ação na plataforma. Para testar se está tudo funcionando, podemos fazer a seguinte requisição:

```
curl -H 'Authorization: Bearer APP_USR-12345678-031820-X-12345678' \
https://api.mercadolibre.com/users/me
```

No header "Authorization", será necessário colocar o access_token que recebemos anteriormente. Caso tudo dê certo, você deve ver as informações da conta autenticada.

Aproveitando este tópico de testes, gostaria de compartilhar como eu fiz para testar minha aplicação, já que seria necessário fazer compras.

Você deve fazer a seguinte requisição para criar um usuário de testes:

```
curl -X POST -H 'Authorization: Bearer $ACCESS_TOKEN' -H "Content-Type: application/json" -d
'{
   	"site_id":"MLB"
}'
https://api.mercadolibre.com/users/test_user

```
a resposta deverá ser algo assim:

```
{
    "id":120506781,
    "nickname":"TEST0548",
    "password":"qatest328",
    "site_status":"active"
}
```

Salve as informações, principalmente os últimos 6 dígitos do ID, você vai precisar colocá-los no lugar do código enviado por e-mail. Vale ressaltar que você pode criar até 10 usuários. Dito isso, precisamos de um cartão de crédito, né? O que adianta ter o usuário mas não conseguir comprar. Uma solução possível é utilizar os cartões de teste que o Mercado Pago disponibiliza. Você pode acessar os dados [aqui](https://www.mercadopago.com.br/developers/pt/docs/salesforce-commerce-cloud/additional-content/your-integrations/test/cards)

Para dúvidas que não foram respondidas, pode deixar nos comentários ou [acessar a documentação](https://developers.mercadolivre.com.br/pt_br/realizacao-de-testes).


## Código

**Controller**


```js
import { Request, Response } from 'express';
import axios from 'axios';
import { NotificationsService } from '../services/notifications';
import { z } from 'zod';
import { env } from '../env';

const notificationsService = new NotificationsService();

export default class NotificationsController {
    async receiveTheRequestFromMercadoLivre(req: Request, res: Response): Promise<Response> {
        const bodySchema = z.object({
            _id: z.string(),
            resource: z.string().startsWith('/orders/'),
            user_id: z.number(),
            topic: z.string().startsWith('orders_v2').endsWith('orders_v2'),
            application_id: z.number(),
            attempts: z.number(),
            sent: z.string(),
            received: z.string(),
            actions: z.array(z.string()).nonempty(),
        });

        const isBodySchema = bodySchema.safeParse(req.body);
        if (!isBodySchema.success) {
            return res.sendStatus(400);
        }

        await axios.post(
         'https://infoprodutos.onrender.com/notification/handle',
            {
                body: req.body,
            },
            {
                headers: {
                    Authorization: env.SECRET,
                },
            },
        );

        return res.sendStatus(200);
    }

    async handleTheRequestFromMercadoLivre(req: Request, res: Response): Promise<Response> {
        const bodySchema = z.object({
            _id: z.string(),
            resource: z.string().startsWith('/orders/'),
            user_id: z.number(),
            topic: z.string().startsWith('orders_v2').endsWith('orders_v2'),
            application_id: z.number(),
            attempts: z.number(),
            sent: z.string(),
            received: z.string(),
            actions: z.array(z.string()).nonempty(),
        });

        const isBodySchema = bodySchema.safeParse(req.body.body);
        if (!isBodySchema.success) {
            return res.sendStatus(400);
        }

        if (req.headers.authorization !== env.SECRET) {
            return res.sendStatus(401);
        }

        await notificationsService.handleRequest(req.body);
        return res.sendStatus(200);
    }
}


```
Como o Mercado Livre fica enviando uma grande quantidade de requisições, resolvi validar através das "actions", já que em algumas delas eles enviam um array vazio e em outras enviam um "status".


**AWS**

```js
import * as AWS from 'aws-sdk';
import { env } from '../../env';

export function createS3Instance() {
    const s3 = new AWS.S3();
    s3.config.update({
        region: env.AWS_BUCKET_REGION,
        secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
        accessKeyId: env.AWS_ACCESS_KEY,
        signatureVersion: 'v4',
    });

    return s3;
}


import { createS3Instance } from './config';
import { env } from '../../env';

export async function getProductFromS3(key: string) {
    const s3 = createS3Instance();

    const expiresInThreeHours = 10800;
    const params = {
        Bucket: env.AWS_BUCKET_NAME,
        Key: key,
        Expires: expiresInThreeHours,
    };

    const url = await s3.getSignedUrlPromise('getObject', params);
    return url;
}

```

**Service**

```js
import { PrismaClient, Tokens } from '@prisma/client';
import { MercadoLivreNotification, Order } from '../@types';
import { updateMercadoLivreRefreshToken } from './refresh-token';

const prisma = new PrismaClient();

export class NotificationsService {
    private async getToken(): Promise<Tokens> {
        const token = await prisma.tokens.findMany({
            orderBy: {
                created_at: 'desc',
            },
            take: 1,
        });

        return token[0];
    }

    private async getOrder(token: Tokens, { body }: MercadoLivreNotification): Promise<Order | void> {
        try {
            const orderRequestDetails = await fetch(`https://api.mercadolibre.com/${body.resource}`, {
                headers: {
                    Authorization: `Bearer ${token.access_token}`,
                },
            });

            if (orderRequestDetails.status >= 400) {
                await updateMercadoLivreRefreshToken();
                return;
            }

            const order: Order = await orderRequestDetails.json();

            if (!order.pack_id) {
                order.pack_id = order.id;
            }
            return order;
        } catch (error) {
            console.log(error);
        }
    }

    async handleRequest(body: MercadoLivreNotification): Promise<void> {
        const token = await this.getToken();

        const order = await this.getOrder(token, body);

        if (!order) {
            return;
        }

        await this.messageSenderHandler(order, token);
    }

    private async messageSenderHandler(order: Order, token: Tokens) {
        const key = this.transformOrderItemInAwsKey(order.order_items[0].item.title);
        const url = await getProductFromS3(key);

        try {
            console.log('chego na mensagem');
            const message = await fetch(
                `https://api.mercadolibre.com/messages/action_guide/packs/${order.pack_id}/option?tag=post_sale`,
                {
                    method: 'post',
                    body: JSON.stringify({
                        option_id: 'OTHER',
                        text: `Obrigado por sua compra, clique no link abaixo para fazer o download do produto:<br><br> ${url}`,
                    }),
                    headers: {
                        Authorization: `Bearer ${token.access_token}`,
                    },
                },
            );

            return message;
        } catch (error) {
            console.log(error);
        }
    }

    private transformOrderItemInAwsKey(name: string): string {
        return name.split(' ').join('_').toLowerCase();
    }
}

```
Como mencionei anteriormente, o token do Mercado Livre expira após 6 horas. Para resolver esse problema, implementei o seguinte código:

```js
import axios, { AxiosResponse } from 'axios';
import { env } from '../env';
import { RefreshToken } from '../@types';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function updateMercadoLivreRefreshToken() {
    const getTheLastTokenRegistered = await prisma.tokens.findMany({
        orderBy: {
            created_at: 'desc',
        },
        take: 1,
    });

    const token: AxiosResponse<RefreshToken> = await axios.post('https://api.mercadolibre.com/oauth/token', {
        grant_type: 'refresh_token',
        client_id: env.MERCADO_LIVRE_APP_ID,
        client_secret: env.MERCADO_LIVRE_SECRET_KEY,
        refresh_token: getTheLastTokenRegistered[0].refresh_token,
    });

    await prisma.tokens.create({
        data: {
            access_token: token.data.access_token,
            refresh_token: token.data.refresh_token,
        },
    });

    return token;
}

```
Basicamente, minha estratégia foi a seguinte: esperar receber um "unauthorized" e então realizar um refresh no token, já que o Mercado Livre enviará uma segunda tentativa da requisição. O primeiro token era introduzido manualmente.


## Conclusão
Obrigado às pessoas que leram até aqui! Sintam-se à vontade para me dar dicas sobre escrita, código ou até mesmo sugestões de algo que poderia ter sido feito de forma diferente, como usar uma stack diferente ou qualquer outra sugestão :D