DICT Service
The DICT Service (apps/dict_service) implements the PIX key directory management layer, fully compliant with BACEN's DICT API v2.10.0. It runs on port 4001 and handles all operations related to PIX key lifecycle, claims, and fraud prevention.
Responsibilities
- PIX key registration, lookup, and removal
- Key portability and ownership claims
- Local DICT mirror synchronization
- Infraction reporting and MED 2.0 special returns
- CID (Compulsory Identifier) management
- Anti-fraud validations
PIX Key Types
The service supports all five PIX key types defined by BACEN:
CPF (Cadastro de Pessoas Fisicas)
Format: 11 numeric digits
Example: 12345678901
Validation: Mod-11 check digits
Limit: 5 keys per CPF across all institutionsCNPJ (Cadastro Nacional da Pessoa Juridica)
Format: 14 numeric digits
Example: 11222333000181
Validation: Mod-11 check digits
Limit: 20 keys per CNPJ across all institutionsPHONE
Format: +55DDDNNNNNNNNN (E.164)
Example: +5511999887766
Validation: Country code must be +55, valid DDD area code
Limit: 5 keys per phone numberEMAIL
Format: Valid email, max 77 characters
Example: user@example.com
Validation: RFC 5322 format
Limit: 5 keys per emailEVP (Endereco Virtual de Pagamento)
Format: UUID v4
Example: 123e4567-e89b-12d3-a456-426614174000
Generation: Server-side random UUID
Limit: 20 per natural person, 20 per legal personKey Lifecycle
Registration
When a new PIX key is registered, the service performs the following steps:
- Validate the key format against the type-specific rules
- Check local uniqueness to prevent duplicate registrations for the same account
- Verify ownership by confirming the account holder matches the key owner
- Submit to BACEN DICT via the mTLS-authenticated API
- Persist locally upon successful BACEN confirmation
- Publish event via NATS for downstream services
defmodule DictService.Keys do
alias Shared.Repo
alias Shared.Schema.PixKey
def create_key(attrs) do
with {:ok, key} <- PixKey.changeset(%PixKey{}, attrs) |> validate_key_limits(),
{:ok, bacen_response} <- submit_to_bacen(key),
{:ok, persisted} <- Repo.insert(apply_bacen_data(key, bacen_response)) do
Shared.Nats.publish("pix.key.created", persisted)
{:ok, persisted}
end
end
defp validate_key_limits(changeset) do
key_type = Ecto.Changeset.get_field(changeset, :key_type)
owner_id = Ecto.Changeset.get_field(changeset, :owner_id)
count = Repo.count_keys_by_owner(owner_id, key_type)
max = max_keys_for_type(key_type)
if count < max,
do: {:ok, changeset},
else: {:error, :key_limit_exceeded}
end
defp max_keys_for_type(:cpf), do: 5
defp max_keys_for_type(:cnpj), do: 20
defp max_keys_for_type(:phone), do: 5
defp max_keys_for_type(:email), do: 5
defp max_keys_for_type(:evp), do: 20
endLookup
Key lookups retrieve account information associated with a PIX key. The service first checks the local cache, then falls back to BACEN:
defmodule DictService.Lookup do
def lookup_key(key_value) do
case Shared.Cache.get("dict:key:#{key_value}") do
{:ok, cached} ->
{:ok, cached}
{:error, :not_found} ->
with {:ok, result} <- Shared.Bacen.Client.get("/api/v2/key/#{encode(key_value)}") do
Shared.Cache.put("dict:key:#{key_value}", result, ttl: :timer.minutes(5))
{:ok, result}
end
end
end
endRemoval
Key removal deletes the key from BACEN DICT and the local database. Removed keys enter a cooling-off period during which they cannot be re-registered by a different participant.
Claims
Claims handle the transfer of PIX keys between institutions (portability) or between accounts at the same institution (ownership).
Portability Claim
When a customer moves their PIX key from one institution to another:
1. New institution (claimer) creates a portability claim
2. BACEN notifies the current institution (donor)
3. Donor has 7 calendar days to confirm or deny
4. If confirmed (or timeout), key transfers to claimer
5. If denied with valid reason, claim is cancelledOwnership Claim
When a PIX key needs to be reassigned to a different account holder:
1. New owner's institution creates an ownership claim
2. BACEN notifies the current key holder's institution
3. Current holder has 14 calendar days to confirm or deny
4. Resolution follows same pattern as portabilityClaim Processing
defmodule DictService.Claims do
def create_claim(type, key_value, claimer_account) do
payload = %{
"KeyType" => key_type(key_value),
"Key" => key_value,
"ClaimType" => claim_type_string(type),
"ClaimerAccount" => format_account(claimer_account)
}
with {:ok, response} <- Shared.Bacen.Client.post("/api/v2/claim", payload) do
claim = %Shared.Schema.Claim{
claim_id: response["ClaimId"],
key_value: key_value,
type: type,
status: :open,
expires_at: parse_expiry(response)
}
{:ok, _} = Shared.Repo.insert(claim)
schedule_claim_reminder(claim)
{:ok, claim}
end
end
def confirm_claim(claim_id, action) when action in [:confirm, :deny, :cancel] do
payload = %{"ClaimAnswer" => String.upcase(to_string(action))}
with {:ok, _} <- Shared.Bacen.Client.put("/api/v2/claim/#{claim_id}", payload),
{:ok, claim} <- Shared.Repo.get(Shared.Schema.Claim, claim_id) do
Shared.Repo.update(Ecto.Changeset.change(claim, status: action))
end
end
endMED 2.0 (Fraud Prevention)
MED (Mecanismo Especial de Devolucao) 2.0 is BACEN's enhanced fraud prevention system. The DICT Service handles:
Infraction Reports
Report suspicious activity on a PIX key or transaction:
defmodule DictService.Infractions do
@infraction_types [:fraud, :request_refund, :account_closure]
def create_infraction(params) do
payload = %{
"EndToEndId" => params.end_to_end_id,
"InfractionType" => format_type(params.type),
"ReportDetails" => params.details,
"CreditedParticipant" => params.credited_ispb,
"DebitedParticipant" => params.debited_ispb
}
with {:ok, response} <- Shared.Bacen.Client.post("/api/v2/infraction", payload) do
persist_infraction(response)
end
end
endSpecial Returns
When an infraction is confirmed as fraud, the platform can trigger a special return of funds:
- BACEN notifies the credited participant's institution
- The institution has a defined window to block and return the funds
- Partial returns are supported when funds have already been moved
- The return is processed through SPI as a pacs.004 message
Anti-Fraud Markers
The service maintains anti-fraud markers on keys and accounts:
- Fraud confirmed - Key is flagged, future registrations are blocked
- Under investigation - Key is monitored, transactions require additional validation
- Cleared - Investigation complete, no fraud found
DICT Synchronization
BACEN requires participants to maintain a synchronized local copy of DICT data. The sync worker runs every 15 minutes:
defmodule DictService.SyncWorker do
use GenServer
require Logger
@sync_interval :timer.minutes(15)
def init(state) do
schedule_sync()
{:ok, state}
end
def handle_info(:sync, state) do
Logger.info("Starting DICT sync from cursor #{state.last_sync_id}")
case sync_batch(state.last_sync_id) do
{:ok, new_cursor, count} ->
Logger.info("DICT sync complete: #{count} changes applied")
schedule_sync()
{:noreply, %{state | last_sync_id: new_cursor}}
{:error, reason} ->
Logger.error("DICT sync failed: #{inspect(reason)}")
schedule_retry()
{:noreply, state}
end
end
defp sync_batch(cursor) do
with {:ok, response} <- Shared.Bacen.Client.get("/api/v2/sync/#{cursor}") do
changes = response["Changes"] || []
Enum.each(changes, &apply_change/1)
{:ok, response["LastCursor"], length(changes)}
end
end
defp apply_change(%{"ChangeType" => "CREATE"} = change), do: upsert_key(change)
defp apply_change(%{"ChangeType" => "UPDATE"} = change), do: upsert_key(change)
defp apply_change(%{"ChangeType" => "DELETE"} = change), do: soft_delete_key(change)
endCID Management
CID (Compulsory Identifier) is a unique identifier assigned by BACEN during key registration. The DICT Service manages CID lifecycle:
- Stores CID alongside the PIX key record
- Includes CID in payment initiation messages
- Validates CID on incoming payments
- Refreshes CID when keys are updated
Telemetry and Monitoring
The DICT Service exposes the following metrics:
| Metric | Type | Description |
|---|---|---|
dict.key.create.count | Counter | Keys registered |
dict.key.create.duration | Histogram | Registration latency |
dict.key.lookup.count | Counter | Key lookups |
dict.key.lookup.cache_hit | Counter | Cache hit rate |
dict.claim.count | Counter | Claims by type and status |
dict.sync.duration | Histogram | Sync cycle duration |
dict.infraction.count | Counter | Infractions reported |
dict.bacen.request.duration | Histogram | BACEN API latency |