Pular para o conteúdo

PHP

O PHP já vem com a extensão cURL, então dá para falar com a API sem instalar dependência nenhuma. Esta página monta um cliente pequeno sobre ext-curl e usa esse cliente em todos os exemplos seguintes. Em projetos que já usam Composer, o mesmo desenho cabe no Guzzle, mostrado em Extras.

Os exemplos pedem PHP 8.1 ou mais recente. Confirme que a extensão cURL está carregada:

Terminal window
php -m | grep curl
Terminal window
export LIGGA_API_KEY='ligga_live_…'
export LIGGA_BASE='https://api.ligga.app/functions/v1/api'

O arquivo abaixo concentra autenticação, montagem de URL e tradução do corpo de erro em exceção.

ligga.php
<?php
declare(strict_types=1);
final class LiggaError extends RuntimeException
{
/** @param array<string, mixed> $problem */
public function __construct(public readonly int $status, public readonly array $problem)
{
parent::__construct(sprintf('HTTP %d: %s', $status, $problem['title'] ?? 'erro sem corpo'), $status);
}
}
final class LiggaClient
{
public function __construct(
private readonly string $apiKey,
private readonly string $base = 'https://api.ligga.app/functions/v1/api',
) {
}
/**
* @param array<string, mixed>|null $body
* @return array<string, mixed>|null
*/
public function request(
string $method,
string $path,
?array $body = null,
?string $idempotencyKey = null,
): ?array {
$headers = [
'Authorization: Bearer ' . $this->apiKey,
'Accept: application/json',
];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
}
if ($idempotencyKey !== null) {
$headers[] = 'Idempotency-Key: ' . $idempotencyKey;
}
$ch = curl_init($this->base . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR));
}
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$erroDeRede = curl_error($ch);
curl_close($ch);
if ($raw === false) {
throw new RuntimeException('Falha de rede: ' . $erroDeRede);
}
if ($status === 204) {
return null;
}
$data = json_decode((string) $raw, true);
if ($status >= 400) {
throw new LiggaError($status, is_array($data) ? $data : []);
}
return $data;
}
public static function doAmbiente(): self
{
return new self(
(string) getenv('LIGGA_API_KEY'),
(string) (getenv('LIGGA_BASE') ?: 'https://api.ligga.app/functions/v1/api'),
);
}
/** @return array<string, mixed>|null */
public function get(string $path): ?array
{
return $this->request('GET', $path);
}
/** @param array<string, mixed> $body */
public function post(string $path, array $body, ?string $idempotencyKey = null): ?array
{
return $this->request('POST', $path, $body, $idempotencyKey);
}
/** @param array<string, mixed> $body */
public function patch(string $path, array $body): ?array
{
return $this->request('PATCH', $path, $body);
}
public function delete(string $path): ?array
{
return $this->request('DELETE', $path);
}
}
function novaChaveIdempotente(): string
{
$bytes = random_bytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
}

GET /v1/me descreve a chave que fez a chamada e confirma que a configuração está correta.

me.php
<?php
require_once 'ligga.php';
$ligga = LiggaClient::doAmbiente();
$me = $ligga->get('/v1/me');
echo $me['data']['team_name'], '', implode(', ', $me['data']['scopes']), PHP_EOL;
{
"data": {
"team_id": "bca09e2f-41ab-4417-b84b-f2ff50f991ca",
"team_name": "Pet Shop Aurora",
"scopes": ["*:read", "sales:write"],
"plan_code": "pro"
},
"meta": { "request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4V" }
}

Uma listagem devolve os itens em data e o estado da paginação em pagination. O cursor da próxima página está em pagination.next_cursor e vem null quando acabou.

clientes.php
<?php
require_once 'ligga.php';
/** @return Generator<array<string, mixed>> */
function listarClientes(LiggaClient $ligga, array $filtros = []): Generator
{
$cursor = null;
do {
$query = http_build_query(
['limit' => 100] + $filtros + ($cursor !== null ? ['cursor' => $cursor] : []),
);
$pagina = $ligga->get("/v1/customers?{$query}");
yield from $pagina['data'];
$cursor = $pagina['pagination']['next_cursor'];
} while ($cursor !== null);
}
$ligga = LiggaClient::doAmbiente();
foreach (listarClientes($ligga, ['filter[is_active][eq]' => 'true']) as $cliente) {
echo $cliente['id'], '', $cliente['full_name'], PHP_EOL;
}

O limit vai de 1 a 100, com padrão 20. pagination.has_more responde a mesma pergunta que o cursor nulo, e serve quando você quer só saber se existe mais alguma coisa.

Gere a chave antes da primeira tentativa e reaproveite o mesmo valor em todas as repetições. Assim, uma requisição perdida no caminho não vira dois registros.

criar_venda.php
<?php
require_once 'ligga.php';
const RETENTAVEIS = [408, 429, 500, 502, 503, 504];
function criarVenda(LiggaClient $ligga, array $payload, int $tentativas = 3): ?array
{
$chave = novaChaveIdempotente();
for ($tentativa = 1; $tentativa <= $tentativas; $tentativa++) {
try {
return $ligga->post('/v1/sales', $payload, $chave);
} catch (LiggaError $erro) {
if (!in_array($erro->status, RETENTAVEIS, true) || $tentativa === $tentativas) {
throw $erro;
}
usleep((int) (250_000 * 2 ** ($tentativa - 1)));
}
}
}
$ligga = LiggaClient::doAmbiente();
$venda = criarVenda($ligga, [
'customer_id' => '7c4b0a19-2f36-4d1e-9b58-1f0a6c3e5d24',
'transaction_type' => 'sale',
'sale_date' => '2026-05-18',
'items' => [
[
'catalog_type' => 'product',
'catalog_item_id' => '3d61f8ba-9c04-4a77-8e12-5b7a0d9f2c68',
'quantity' => 2,
'unit_price' => 29.90,
],
],
]);
echo $venda['data']['id'], PHP_EOL;

Reusar a mesma chave com um corpo diferente devolve 409 idempotency_conflict: é sinal de que a chave vazou para outra operação.

Respostas de erro vêm como application/problem+json. O cliente acima já as converte em LiggaError, com o corpo inteiro em $erro->problem.

tratar_erro.php
<?php
require_once 'ligga.php';
$ligga = LiggaClient::doAmbiente();
try {
$ligga->post('/v1/customers', []);
} catch (LiggaError $erro) {
echo $erro->problem['status'], ' ', $erro->problem['type'], PHP_EOL;
echo $erro->problem['detail'] ?? $erro->problem['title'], PHP_EOL;
echo 'request_id: ', $erro->problem['request_id'], PHP_EOL;
foreach ($erro->problem['errors'] ?? [] as $campo) {
echo ' - ', $campo['field'], ' ', $campo['code'], ' ', $campo['message'], PHP_EOL;
}
}
{
"type": "https://api.ligga.app/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "full_name is required",
"request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4V"
}

type, title, status e request_id estão sempre presentes; detail, instance e errors aparecem quando fazem sentido. Guarde o request_id no seu log: é o que o time da Ligga pede para investigar uma chamada.

HTTPtypeO que fazer
401invalid_tokenConfira LIGGA_API_KEY e o cabeçalho Authorization
403insufficient_scopeA chave não carrega o escopo do recurso
403module_not_enabledO módulo do recurso não está no contrato
409idempotency_conflictA mesma Idempotency-Key foi reusada com outro corpo
422validationCampo obrigatório ausente ou com tipo errado
429rate-limit-exceededEspere o tempo do cabeçalho Retry-After

Em projetos com Composer, o Guzzle poupa o trabalho de configurar cURL na mão. A URL base termina com barra e os caminhos entram sem a barra inicial:

Terminal window
composer require guzzlehttp/guzzle
guzzle.php
<?php
require_once 'vendor/autoload.php';
require_once 'ligga.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
$client = new Client([
'base_uri' => 'https://api.ligga.app/functions/v1/api/',
'headers' => ['Authorization' => 'Bearer ' . getenv('LIGGA_API_KEY')],
'timeout' => 30,
]);
try {
$res = $client->post('v1/customers', [
'headers' => ['Idempotency-Key' => novaChaveIdempotente()],
'json' => ['full_name' => 'Ana Ribeiro', 'email' => 'ana@example.com'],
]);
$cliente = json_decode((string) $res->getBody(), true);
echo $cliente['data']['id'], PHP_EOL;
} catch (ClientException $erro) {
$problema = json_decode((string) $erro->getResponse()->getBody(), true);
echo $problema['type'], '', $problema['request_id'], PHP_EOL;
}