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.
Configuração
Seção intitulada “Configuração”Os exemplos pedem PHP 8.1 ou mais recente. Confirme que a extensão cURL está carregada:
php -m | grep curlexport 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.
<?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));}Primeira requisição
Seção intitulada “Primeira requisição”GET /v1/me descreve a chave que fez a chamada e confirma que a configuração
está correta.
<?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" }}Listar com paginação
Seção intitulada “Listar com paginação”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.
<?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.
Escrita idempotente
Seção intitulada “Escrita idempotente”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.
<?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.
Tratamento de erros
Seção intitulada “Tratamento de erros”Respostas de erro vêm como application/problem+json. O cliente acima já as
converte em LiggaError, com o corpo inteiro em $erro->problem.
<?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.
| HTTP | type | O que fazer |
|---|---|---|
| 401 | invalid_token | Confira LIGGA_API_KEY e o cabeçalho Authorization |
| 403 | insufficient_scope | A chave não carrega o escopo do recurso |
| 403 | module_not_enabled | O módulo do recurso não está no contrato |
| 409 | idempotency_conflict | A mesma Idempotency-Key foi reusada com outro corpo |
| 422 | validation | Campo obrigatório ausente ou com tipo errado |
| 429 | rate-limit-exceeded | Espere 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:
composer require guzzlehttp/guzzle<?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;}Veja também
Seção intitulada “Veja também”- Início rápido — a primeira chamada, passo a passo
- Paginação, filtros e ordenação — operadores e
sort - Idempotência — o ciclo de vida da
Idempotency-Key - Erros — o catálogo completo de
type