Ligar com o Vero (OAuth 2.0)
A sua plataforma (loja online, marketplace, POS) quer emitir facturas certificadas pela AGT em nome dos comerciantes que a usam? Cada comerciante tem a sua conta Vero e liga-a à sua plataforma com um clique: escolhe a empresa, vê as permissões e autoriza. Você recebe um token só para essa empresa e emite pela API do Vero. A sua plataforma nunca vê as chaves AGT nem a palavra-passe do comerciante, e ele pode desligar quando quiser.
Antes de começar
- No painel do Vero, abra Chaves de API → Apps OAuth → Registar app. Indique o nome e o logótipo (aparecem ao comerciante), o site, os endereços de retorno (
redirect_uri, https; numa app Test também http://localhost, para testar no seu computador) e as permissões. Não precisa de pedir ao Vero. - Recebe na hora um
client_ide umclient_secretde teste. Em teste, as facturas são emitidas em sandbox: nada vai à AGT. - Quando a integração estiver pronta, registe uma app live. Para isso a sua empresa tem de estar activa na AGT. Até ser verificada pelo Vero, uma app live liga no máximo 100 empresas e o ecrã de autorização avisa que ainda não foi verificada; peça a verificação ao suporte quando precisar de mais.
Metadados (RFC 8414): https://api.vero.ao/.well-known/oauth-authorization-server. Todos os endpoints na referência da API OAuth.
1. Enviar o comerciante para o Vero
Gere um code_verifier aleatório (PKCE, obrigatório), guarde-o na sessão do comerciante e redireccione o browser. O state protege contra CSRF: confirme-o no retorno.
import { createHash, randomBytes } from 'node:crypto'
const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier).digest('base64url')
const state = randomBytes(16).toString('hex')
session.vero = { verifier, state }
const url = new URL('https://vero.ao/oauth/authorize')
url.search = new URLSearchParams({
response_type: 'code',
client_id: process.env.VERO_CLIENT_ID,
redirect_uri: 'https://loja.exemplo.ao/vero/callback',
scope: 'invoices:write customers:write',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString()
res.redirect(url.toString())No Vero, o comerciante entra (ou cria conta), escolhe a empresa e autoriza. Numa app live, a empresa tem de ter as chaves AGT; o próprio ecrã leva-o a configurá-las.
2. Trocar o código por tokens
O Vero volta ao seu redirect_uri com ?code=...&state=... (ou ?error=access_denied se recusou). O código vale 60 segundos e só se usa uma vez. Troque-o no servidor:
curl -X POST https://api.vero.ao/oauth/token \
-u "$VERO_CLIENT_ID:$VERO_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=vero_oac_... \
-d redirect_uri=https://loja.exemplo.ao/vero/callback \
-d code_verifier=O_VERIFIER_GUARDADO{
"access_token": "vero_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "vero_ort_...",
"scope": "invoices:write customers:write",
"organisation": { "id": "8f1c...", "name": "Padaria Kianda, Lda", "taxId": "5000123456" }
}Guarde os tokens e o organisation.id associados ao comerciante, no servidor (nunca no browser).
3. Emitir
É a mesma API das chaves de API. Use sempre uma idempotencyKey (o id da encomenda): repetir o pedido devolve a mesma factura.
curl -X POST https://api.vero.ao/v1/organisations/ORG_ID/invoices \
-H "Authorization: Bearer vero_oat_..." \
-H "Content-Type: application/json" \
-d '{
"customerId": "...",
"documentType": "FR",
"items": [{ "description": "Encomenda #8812", "quantity": 1, "unitPrice": 2500000, "taxRate": 14 }],
"idempotencyKey": "encomenda-8812"
}'A factura aparece no Vero do comerciante como "Emitida por" a sua app. O estado da AGT chega por webhook (passo 5) ou em GET /v1/organisations/ORG_ID/invoices/ID (com invoices:read).
4. Renovar e desligar
O access token dura 1 hora. Renove-o com o refresh token: recebe um par novo e o anterior deixa de valer. Um refresh token já usado que volte a aparecer desliga a empresa por segurança. O refresh token expira após 90 dias sem uso.
curl -X POST https://api.vero.ao/oauth/token \
-u "$VERO_CLIENT_ID:$VERO_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token=vero_ort_...
# O comerciante desligou na sua plataforma: revogue (desliga também no Vero)
curl -X POST https://api.vero.ao/oauth/revoke \
-u "$VERO_CLIENT_ID:$VERO_CLIENT_SECRET" \
-d token=vero_ort_...GET /v1/oauth/token-info devolve a empresa, as permissões e o ambiente do token em uso.
5. Webhooks: estado AGT e ligações desligadas
A validação da AGT é assíncrona. Em vez de consultar, indique-nos um URL de webhook: recebe um POST por evento, para todas as empresas ligadas à sua app (o orgId diz qual). Responda 2xx em até 10 segundos; se falhar, voltamos a tentar ao fim de 1 min, 5 min, 30 min, 2 h e 8 h. O mesmo evento pode chegar mais do que uma vez: use o id para não o tratar duas vezes.
| invoice.agt_validated | A AGT validou uma factura emitida pela sua app (ou qualquer factura, com invoices:read). |
| invoice.agt_rejected | A AGT rejeitou a factura; data.agtErrors traz os motivos. |
| credit_note.agt_validated / _rejected | O mesmo para notas de crédito (com credit_notes:write ou invoices:read). |
| app.authorization.revoked | A empresa desligou a sua app (ou o Vero desligou por segurança). Os tokens já não valem. |
| ping | Evento de teste, enviado a pedido. |
{
"id": "5b0e7c1e-...",
"event": "invoice.agt_validated",
"orgId": "8f1c...",
"data": {
"id": "c2a9...", "number": "FR 2026/1532", "documentType": "FR",
"total": 2850000, "currency": "AOA", "agtStatus": "validated",
"agtErrors": null, "issuedByThisApp": true
},
"timestamp": "2026-10-15T14:02:11.000Z"
}Cada pedido traz X-Vero-Event, X-Vero-Delivery (o id do evento) e X-Vero-Signature: o HMAC SHA-256 do corpo em bruto com o segredo whsec_... que lhe entregamos. Verifique sempre antes de confiar no evento:
import { createHmac, timingSafeEqual } from 'node:crypto'
// Use o corpo EM BRUTO (antes do JSON.parse)
function veroSignatureOk(rawBody: string, header: string | undefined) {
const expected = 'sha256=' + createHmac('sha256', process.env.VERO_WEBHOOK_SECRET!).update(rawBody).digest('hex')
return !!header && header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected))
}Permissões (scope)
| organisation:read | Dados da empresa em GET /v1/organisations/:id: nome, NIF, morada, regime de IVA e se está activada na AGT. |
| invoices:read | Listar e abrir facturas, recibos, notas de crédito e PDFs. |
| invoices:write | Emitir facturas (FT/FR) e recibos, e enviá-los por email. Comunicado à AGT. |
| credit_notes:write | Emitir notas de crédito (devoluções). Comunicado à AGT. Pedir só se precisar. |
| customers:read | Ver clientes. |
| customers:write | Criar e actualizar clientes (inclui ver). |
| products:read | Ver produtos. |
| products:write | Criar e actualizar produtos (inclui ver). |
Sem scope, pede as permissões da app sem as notas de crédito. Um token OAuth nunca acede a chaves AGT, chaves de API, definições, equipa nem pagamentos, e não cria empresas.
O comerciante decide. No ecrã de autorização pode desmarcar permissões que pediu, e mais tarde retirar outras em Programador → Aplicações ligadas, sem desligar a app. O scope da resposta do token diz as que recebeu; quando ele retira uma, as rotas dela passam a responder 403 insufficient_scope com o mesmo token. Confirme as permissões em GET /v1/oauth/token-info e funcione com o que tiver (por exemplo, sem customers:write, use o Consumidor Final).
Dados da empresa (organisation:read)
GET /v1/organisations/:id
{
"id": "8f1c...",
"name": "Padaria Kianda",
"legalName": "Padaria Kianda, Lda",
"taxId": "5000123456",
"email": "geral@kianda.ao",
"phone": "+244 923 000 000",
"website": null,
"addressLine1": "Rua Rainha Ginga, 12",
"addressLine2": null,
"city": "Luanda",
"province": "Luanda",
"country": "AO",
"logoUrl": "https://...",
"eacCode": "10711",
"ivaRegime": "geral", // geral | reduzido | simplificado | isento | null
"defaultTaxRate": 14, // 0 | 5 | 14 | null
"taxExemptionCode": null, // motivo de isenção AGT da empresa (ex.: "M10")
"status": "active",
"agtKeysConfigured": true,
"partialPaymentsEnabled": true // sempre true: recibos parciais aceites em todas as empresas
}Numa empresa isento ou simplificado, envie as linhas a 0%. Se uma linha a 0% vier sem taxExemptionCode, o Vero usa o da empresa. Contas bancárias, plano e dados do dono não são partilhados com apps.
Erros
| 401 | invalid_token | Token expirado ou a empresa desligou a app. Renove com o refresh token; se falhar, peça para ligar de novo. |
| 403 | insufficient_scope | A rota precisa de uma permissão que a empresa não aceitou ou retirou depois. Veja as que tem em GET /v1/oauth/token-info. |
| 403 | forbidden | O token é de outra empresa. |
| 402 | plan_expired | O plano Vero do comerciante expirou: avise-o para renovar. |
| 422 | agt_keys_not_configured | A empresa ainda não tem as chaves AGT. |
| 403 | app_connection_limit | No ecrã de autorização: a app live por verificar já liga 100 empresas. Peça a verificação ao suporte. |