Brazil's central bank (Banco Central do Brasil, BCB) now publishes two lists that crypto compliance teams have been waiting for:
- Companies that applied for authorisation as virtual asset service providers (SPSAVs) under the transition rule in article 88 of Resolução BCB nº 520.
- Institutions the BCB already authorises (banks, brokers, distributors) that told it they will offer virtual asset services under article 22 of the same resolution.
Both sit on the BCB's authorisation page. The page is rendered from two JSON endpoints, so you can read them directly. This post shows a short script, the four things that tripped me up, and why the lists matter this month.
All numbers come from the endpoints as I read them on 10 November 2026, around 10:05 Asia/Shanghai time (UTC+8). The lists will change, so re-run the code before you quote anything.
The endpoints
https://www.bcb.gov.br/api/servico/sitebcb/SPSVAs_autorizacao
https://www.bcb.gov.br/api/servico/sitebcb/ifs-autorizadas-ativos-virtuais
No key, no login. Each returns an object with a single conteudo array. These endpoints feed a public web page; they are not a documented open-data API. So cache the result, poll politely, and expect the shape to change without notice.
The script
import re
import unicodedata
from collections import Counter
import requests
BASE = "https://www.bcb.gov.br/api/servico/sitebcb/"
def fetch(endpoint: str) -> list[dict]:
r = requests.get(BASE + endpoint, timeout=30)
r.raise_for_status()
return r.json()["conteudo"]
applicants = fetch("SPSVAs_autorizacao") # art. 88 applications
incumbents = fetch("ifs-autorizadas-ativos-virtuais") # art. 22 institutions
print(len(applicants), "applications |", len(incumbents), "art. 22 institutions")
print(Counter(a["Modalidade"] for a in applicants))
print(Counter(a["Fase"] for a in applicants))
print(Counter(i["TipoAtuacao"] for i in incumbents))
Output on 10 November:
120 applications | 24 art. 22 institutions
Counter({'Intermediária': 73, 'Corretora': 46, 'Custodiante': 1})
Counter({'Em exame': 119, 'Indeferido': 1})
Counter({'Intermediação': 16, 'Custódia/Intermediação': 8})
So 120 applications: 73 for the intermediary modality, 46 for broker and one for custodian. 119 are "em exame" (under examination) and one is "indeferido" (rejected). On the second list, 8 institutions are shown for custody and intermediation and 16 for intermediation only.
Four traps
1. The two lists format the company ID differently
Brazilian companies are identified by a CNPJ. The first eight digits (the raiz, or root) identify the company; the rest identify the branch. Both lists give only the root, but not in the same format:
print(Counter(len(a["CNPJ"]) for a in applicants)) # Counter({10: 120}) e.g. "39.695.312"
print(Counter(len(i["CNPJ"]) for i in incumbents)) # Counter({8: 24}) e.g. "60701190"
The applications list uses dots; the article 22 list uses bare digits. A naive join on the CNPJ field returns zero matches, and you will not get an error. Normalise to eight digits first:
def cnpj_root(value: str) -> str:
digits = re.sub(r"\D", "", value)
if len(digits) == 14: # a full CNPJ, e.g. from your own KYC data
digits = digits[:8]
return digits.zfill(8)
The len == 14 branch matters because your own records probably hold full CNPJs (60.701.190/0001-04), not roots.
2. Leading zeros are real
The article 22 list includes roots that start with zero. One of them is 00000000, which is Banco do Brasil. The JSON keeps them as strings, so you are fine until you save to CSV and read it back:
import pandas as pd
pd.read_csv("roots.csv")["cnpj"].tolist() # [0, 9516419]
pd.read_csv("roots.csv", dtype=str)["cnpj"].tolist() # ['00000000', '09516419']
Always pass dtype=str for ID columns, and keep the zfill(8) in cnpj_root as a second line of defence.
3. Names have stray whitespace
Four of the 120 company names contain a double space or a trailing tab, for example 'WEBULL Sociedade Prestadora de Serviços de Ativos Virtuais Ltda.\t'. If you compare names, collapse whitespace first:
def clean_name(value: str) -> str:
return re.sub(r"\s+", " ", value).strip()
4. You can't tell an SPSAV by its name
Article 15 of Resolution 520 requires an SPSAV to carry the expression "Sociedade Prestadora de Serviços de Ativos Virtuais" in its company name, and most applicants already do. But matching on the phrase gives different answers depending on how strict you are:
def fold(value: str) -> str:
s = unicodedata.normalize("NFKD", clean_name(value)).casefold()
return "".join(c for c in s if not unicodedata.combining(c))
phrase = "Sociedade Prestadora de Serviços de Ativos Virtuais"
exact = sum(phrase in a["Title"] for a in applicants)
folded = sum(fold(phrase) in fold(a["Title"]) for a in applicants)
print(exact, folded) # 99 104
The five names that only the folded match finds are two written in capitals, two without the cedilla ("Servicos") and one with the double space from trap 3. The other 16 applicants are listed under names without the phrase at all ("... Serviços Digitais Ltda.", "... Securitizadora S.A." and similar). Do not use the phrase as a filter. Treat the list as the source of truth, keyed by CNPJ root.
A lookup you can put in front of onboarding
index = {}
for a in applicants:
index[cnpj_root(a["CNPJ"])] = ("art. 88 application", a["Modalidade"], a["Fase"], clean_name(a["Title"]))
for i in incumbents:
index[cnpj_root(i["CNPJ"])] = ("art. 22 institution", i["TipoAtuacao"], None, clean_name(i["Title"]))
def lookup(cnpj: str):
return index.get(cnpj_root(cnpj), "not on either list")
print(len(index)) # 144: no root appears on both lists
print(lookup("60.701.190/0001-04")) # ('art. 22 institution', 'Custódia/Intermediação', None, 'ITAÚ UNIBANCO S.A.')
A few rules for using the result:
- "Em exame" is not a licence. It means an application is on file and the process has not been concluded. Resolution 520 treats the two differently: an on-time applicant may keep operating "until the conclusion of its authorisation process" (art. 88, para. 6), which is not the same as being authorised.
- "Indeferido" needs a human. The list gives no reasons.
- "Not on either list" is not proof of anything on its own. Check the spelling of the CNPJ, re-fetch, and look at other BCB sources before drawing a conclusion.
- Store the snapshot with a timestamp. If a decision is ever questioned, you want to show what the list said on the day you checked.
Why this matters in November
Two dates in Resolution 520 make these lists practical tools rather than trivia:
- Article 91, as reworded by Resolução BCB nº 589, has applied since 6 November 2026. BCB-authorised institutions may not carry out or enable virtual asset operations whose counterparties are virtual asset service providers not authorised by the BCB, except in forms the regulation expressly allows. In its Vote 108/2026, the BCB said it moved this date from 30 October so it could publish an official list of institutions in transition.
- Article 88, paragraph 7: providers that did not apply by the 30 October deadline must stop providing virtual asset services within 30 days after it. By our count that is 29 November 2026. Under paragraph 6, providers that applied on time may keep operating until their process is concluded, but may not switch modality in the meantime.
If you build payment, banking or exchange tooling for Brazil, the CNPJ lookup above is the cheapest control you can add before 29 November. For the wider timeline, VASP Tracker keeps a Brazil page that follows the same BCB sources, also in Portuguese.
Official texts: Resolução BCB nº 520, Resolução BCB nº 589, Voto 108/2026.
This is not legal advice. The BCB's own pages and the resolutions are the authoritative sources; check them before relying on anything here. VASP Tracker is an independent tracker, not an official source, and is not affiliated with any regulator.
Top comments (0)