Criar venda com itens
O que você vai construir
Seção intitulada “O que você vai construir”Uma venda com dois itens, um produto e um serviço, criada em uma única
requisição a POST /v1/sales e protegida por Idempotency-Key.
No caminho você vê onde encontrar os identificadores que o corpo exige, como o
servidor calcula subtotal e total_amount, com que status a venda nasce e
como conferir o histórico de status depois.
Pré-requisitos
Seção intitulada “Pré-requisitos”- Uma chave de API com o escopo
sales:writee o módulovendashabilitado no contrato. Confira os dois emGET /v1/me. - Pelo menos um item de catálogo cadastrado, produto ou serviço. A venda aponta
para ele por
catalog_item_id. - Um cliente, se você quiser vincular a venda a alguém.
customer_idé opcional: venda de balcão pode ir sem cliente.
export LIGGA_API_KEY='ligga_live_…'export LIGGA_BASE='https://api.ligga.app/functions/v1/api'Passo a passo
Seção intitulada “Passo a passo”-
Confirme escopo e módulo. Sem
sales:writea resposta é403 insufficient_scope; sem o módulovendasno contrato, é403 module_not_enabled:Terminal window curl "$LIGGA_BASE/v1/me" \-H "Authorization: Bearer $LIGGA_API_KEY" -
Encontre o cliente. Busque pelo dado que você já tem e guarde o
id:Terminal window curl -G "$LIGGA_BASE/v1/customers" \-H "Authorization: Bearer $LIGGA_API_KEY" \--data-urlencode 'filter[email][eq]=maria@exemplo.com' -
Encontre os itens do catálogo. Produtos e serviços são recursos separados, e o
catalog_typede cada item da venda diz de qual dos dois ocatalog_item_idveio:Terminal window curl -G "$LIGGA_BASE/v1/products" \-H "Authorization: Bearer $LIGGA_API_KEY" \--data-urlencode 'filter[name][ilike]=%ração%'curl -G "$LIGGA_BASE/v1/services" \-H "Authorization: Bearer $LIGGA_API_KEY" \--data-urlencode 'filter[name][ilike]=%banho%' -
Escolha a chave de idempotência. Use o identificador que o seu sistema já dá ao pedido, não um valor sorteado a cada tentativa. É ele que garante que uma nova tentativa não vire uma segunda venda.
-
Crie a venda. Os itens vão aninhados no mesmo corpo. A resposta é
201 Createde já trazitems:Terminal window curl -X POST "$LIGGA_BASE/v1/sales" \-H "Authorization: Bearer $LIGGA_API_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: pedido-loja-8842" \-d '{"customer_id": "9c8d7e6f-1a2b-4c3d-8e9f-0a1b2c3d4e5f","transaction_type": "sale","payment_method": "pix","payment_status": "pending","items": [{"catalog_type": "product","catalog_item_id": "b7e1f3a2-8c4d-4e5f-9a6b-1c2d3e4f5a6b","quantity": 2,"unit_price": 49.9},{"catalog_type": "service","catalog_item_id": "0d9e8f7a-6b5c-4d3e-2f1a-9b8c7d6e5f4a","quantity": 1,"unit_price": 90,"discount_amount": 10}]}'{"data": {"id": "3f9a1b7e-2c4d-4e8f-b6a1-5d0c9e8f7a2b","customer_id": "9c8d7e6f-1a2b-4c3d-8e9f-0a1b2c3d4e5f","transaction_type": "sale","transaction_number": "000231","status": "pending","sale_date": "2026-07-09T18:40:00.000Z","delivery_date": null,"subtotal": 189.8,"discount_amount": null,"discount_percentage": null,"tax_amount": null,"total_amount": 179.8,"notes": null,"payment_method": "pix","payment_status": "pending","delivery_fee": null,"order_code": null,"is_active": true,"created_at": "2026-07-09T18:40:02.000Z","updated_at": null,"items": [{"id": "5b1c9a7d-3e2f-4a8b-9c0d-1e2f3a4b5c6d","catalog_type": "product","catalog_item_id": "b7e1f3a2-8c4d-4e5f-9a6b-1c2d3e4f5a6b","quantity": 2,"unit_price": 49.9,"discount_amount": null,"discount_percentage": null,"subtotal": 99.8,"total_amount": 99.8,"notes": null,"created_at": "2026-07-09T18:40:02.000Z"},{"id": "7d2e0b8c-4f3a-4b9c-8d1e-2f3a4b5c6d7e","catalog_type": "service","catalog_item_id": "0d9e8f7a-6b5c-4d3e-2f1a-9b8c7d6e5f4a","quantity": 1,"unit_price": 90,"discount_amount": 10,"discount_percentage": null,"subtotal": 90,"total_amount": 80,"notes": null,"created_at": "2026-07-09T18:40:02.000Z"}]},"meta": { "request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4V" }} -
Confira o histórico de status. O histórico não vem na resposta da criação. Peça por
includena busca porid:Terminal window curl -G "$LIGGA_BASE/v1/sales/3f9a1b7e-2c4d-4e8f-b6a1-5d0c9e8f7a2b" \-H "Authorization: Bearer $LIGGA_API_KEY" \--data-urlencode 'include=items,status_history'{"data": {"id": "3f9a1b7e-2c4d-4e8f-b6a1-5d0c9e8f7a2b","status": "pending","status_history": [{"id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f","old_status": null,"new_status": "pending","change_reason": "created via public api","created_at": "2026-07-09T18:40:02.000Z"}]},"meta": { "request_id": "req_01JZXQ8V2K5M7N9P0R1S2T3U4W" }} -
Avance o status quando o pedido andar.
PATCHaltera apenasstatus,payment_status,payment_method,notes,delivery_dateedelivery_fee, e cada mudança destatusgrava uma entrada nova no histórico:Terminal window curl -X PATCH "$LIGGA_BASE/v1/sales/3f9a1b7e-2c4d-4e8f-b6a1-5d0c9e8f7a2b" \-H "Authorization: Bearer $LIGGA_API_KEY" \-H "Content-Type: application/json" \-d '{ "status": "completed", "payment_status": "paid" }'
Código completo
Seção intitulada “Código completo”#!/usr/bin/env bashset -euo pipefail
: "${LIGGA_API_KEY:?defina LIGGA_API_KEY}": "${LIGGA_BASE:?defina LIGGA_BASE}"
CUSTOMER_ID='9c8d7e6f-1a2b-4c3d-8e9f-0a1b2c3d4e5f'PRODUTO_ID='b7e1f3a2-8c4d-4e5f-9a6b-1c2d3e4f5a6b'SERVICO_ID='0d9e8f7a-6b5c-4d3e-2f1a-9b8c7d6e5f4a'PEDIDO='pedido-loja-8842'
VENDA=$(curl -sS -X POST "$LIGGA_BASE/v1/sales" \ -H "Authorization: Bearer $LIGGA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $PEDIDO" \ -d "{ \"customer_id\": \"$CUSTOMER_ID\", \"transaction_type\": \"sale\", \"payment_method\": \"pix\", \"payment_status\": \"pending\", \"items\": [ { \"catalog_type\": \"product\", \"catalog_item_id\": \"$PRODUTO_ID\", \"quantity\": 2, \"unit_price\": 49.9 }, { \"catalog_type\": \"service\", \"catalog_item_id\": \"$SERVICO_ID\", \"quantity\": 1, \"unit_price\": 90, \"discount_amount\": 10 } ] }")
VENDA_ID=$(echo "$VENDA" | jq -r '.data.id')echo "venda criada: $VENDA_ID"echo "$VENDA" | jq '{ subtotal: .data.subtotal, total: .data.total_amount, status: .data.status }'
curl -sS -G "$LIGGA_BASE/v1/sales/$VENDA_ID" \ -H "Authorization: Bearer $LIGGA_API_KEY" \ --data-urlencode 'include=items,status_history' \ | jq '.data.status_history'Rodar o script duas vezes com o mesmo PEDIDO não cria duas vendas: a segunda
execução recebe a resposta guardada da primeira, com o cabeçalho
Idempotent-Replay: true.
Decisões
Seção intitulada “Decisões”A venda nasce em pending, não em completed. status é opcional no
corpo e, quando você não manda nada, o servidor grava pending. Deixe assim se
o pagamento ainda não foi confirmado e avance por PATCH quando ele for. Mandar
"status": "completed" na criação é válido, mas aí a venda não passa pelos
estados intermediários e o histórico registra só o estado final.
Os totais são calculados no servidor. Você envia quantity, unit_price e,
se houver, discount_amount e discount_percentage. O servidor calcula, para
cada item, subtotal = quantity × unit_price e
total_amount = subtotal − desconto percentual − desconto em valor, nunca
negativo. Na venda, subtotal é a soma dos subtotais dos itens e total_amount
é a soma dos totais dos itens mais delivery_fee. Campos de total enviados no
corpo são simplesmente descartados.
catalog_type diz de onde veio o item. Os valores aceitos são product,
service e package. Ele precisa combinar com a origem do catalog_item_id:
um identificador de GET /v1/services com catalog_type: "product" grava um
item que aponta para o lugar errado.
A chave de idempotência é o número do pedido. Assim, uma nova tentativa
depois de um timeout devolve a venda original em vez de criar outra. Reusar a
mesma chave com um corpo diferente responde 409 idempotency_conflict, e isso é
sinal de defeito no seu lado, não da API.
Os itens não são editáveis depois. PATCH só alcança os campos de topo. Para
corrigir quantidade, preço ou composição, cancele a venda com
DELETE /v1/sales/{id}, que define status: "cancelled" sem apagar o registro, e
crie outra venda com uma chave de idempotência nova.
Erros comuns
Seção intitulada “Erros comuns”Quando a criação falha, o corpo vem em application/problem+json e o campo que
identifica a causa é type, nunca code:
| HTTP | type | Quando acontece |
|---|---|---|
| 401 | invalid_token | Cabeçalho Authorization ausente ou chave inexistente |
| 403 | insufficient_scope | A chave não tem sales:write nem *:write |
| 403 | module_not_enabled | O módulo vendas não está habilitado no contrato |
| 409 | idempotency_conflict | Mesma Idempotency-Key reenviada com corpo diferente |
| 422 | validation | items vazio ou com mais de 200 entradas; quantity igual ou menor que zero; unit_price negativo; catalog_type fora de product, service e package; catalog_item_id que não é um UUID; discount_percentage fora da faixa de 0 a 100 |
| 429 | rate-limit-exceeded | Cota de escrita do plano excedida |
Quando não usar
Seção intitulada “Quando não usar”- A operação não é uma venda. Compras e pagamentos a fornecedor vivem em Despesas, com outro conjunto de campos.
- O item não está no catálogo. Todo item precisa de um
catalog_item_idexistente. A API não cria produtos nem serviços: cadastre no app Ligga antes. - Você precisa corrigir uma venda existente sem perder o registro. Cancelar
e recriar troca o
ide o número da transação. Se isso for um problema para a sua conciliação, ajuste apenas o que oPATCHalcança.
Veja também
Seção intitulada “Veja também”- Vendas (sales) — todos os campos, filtros e a semântica do
DELETE - Produtos (products) — de onde vem o
catalog_item_iddos produtos - Idempotência — como escolher e reusar a chave
- Erros — o formato completo do corpo de erro