Plataformas

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

  1. 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.
  2. Recebe na hora um client_id e um client_secret de teste. Em teste, as facturas são emitidas em sandbox: nada vai à AGT.
  3. 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.

TypeScript
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:

Terminal
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
JSON
{
  "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.

Terminal
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.

Terminal
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_validatedA AGT validou uma factura emitida pela sua app (ou qualquer factura, com invoices:read).
invoice.agt_rejectedA AGT rejeitou a factura; data.agtErrors traz os motivos.
credit_note.agt_validated / _rejectedO mesmo para notas de crédito (com credit_notes:write ou invoices:read).
app.authorization.revokedA empresa desligou a sua app (ou o Vero desligou por segurança). Os tokens já não valem.
pingEvento de teste, enviado a pedido.
JSON
{
  "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:

TypeScript
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:readDados da empresa em GET /v1/organisations/:id: nome, NIF, morada, regime de IVA e se está activada na AGT.
invoices:readListar e abrir facturas, recibos, notas de crédito e PDFs.
invoices:writeEmitir facturas (FT/FR) e recibos, e enviá-los por email. Comunicado à AGT.
credit_notes:writeEmitir notas de crédito (devoluções). Comunicado à AGT. Pedir só se precisar.
customers:readVer clientes.
customers:writeCriar e actualizar clientes (inclui ver).
products:readVer produtos.
products:writeCriar 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)

JSON
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

401invalid_tokenToken expirado ou a empresa desligou a app. Renove com o refresh token; se falhar, peça para ligar de novo.
403insufficient_scopeA 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.
403forbiddenO token é de outra empresa.
402plan_expiredO plano Vero do comerciante expirou: avise-o para renovar.
422agt_keys_not_configuredA empresa ainda não tem as chaves AGT.
403app_connection_limitNo ecrã de autorização: a app live por verificar já liga 100 empresas. Peça a verificação ao suporte.