Skip to content

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 institutions

CNPJ (Cadastro Nacional da Pessoa Juridica)

Format: 14 numeric digits
Example: 11222333000181
Validation: Mod-11 check digits
Limit: 20 keys per CNPJ across all institutions

PHONE

Format: +55DDDNNNNNNNNN (E.164)
Example: +5511999887766
Validation: Country code must be +55, valid DDD area code
Limit: 5 keys per phone number

EMAIL

Format: Valid email, max 77 characters
Example: user@example.com
Validation: RFC 5322 format
Limit: 5 keys per email

EVP (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 person

Key Lifecycle

Registration

When a new PIX key is registered, the service performs the following steps:

  1. Validate the key format against the type-specific rules
  2. Check local uniqueness to prevent duplicate registrations for the same account
  3. Verify ownership by confirming the account holder matches the key owner
  4. Submit to BACEN DICT via the mTLS-authenticated API
  5. Persist locally upon successful BACEN confirmation
  6. Publish event via NATS for downstream services
elixir
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
end

Lookup

Key lookups retrieve account information associated with a PIX key. The service first checks the local cache, then falls back to BACEN:

elixir
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
end

Removal

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 cancelled

Ownership 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 portability

Claim Processing

elixir
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
end

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

elixir
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
end

Special Returns

When an infraction is confirmed as fraud, the platform can trigger a special return of funds:

  1. BACEN notifies the credited participant's institution
  2. The institution has a defined window to block and return the funds
  3. Partial returns are supported when funds have already been moved
  4. 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:

elixir
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)
end

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

MetricTypeDescription
dict.key.create.countCounterKeys registered
dict.key.create.durationHistogramRegistration latency
dict.key.lookup.countCounterKey lookups
dict.key.lookup.cache_hitCounterCache hit rate
dict.claim.countCounterClaims by type and status
dict.sync.durationHistogramSync cycle duration
dict.infraction.countCounterInfractions reported
dict.bacen.request.durationHistogramBACEN API latency

FluxiQ PIX - Plataforma Brasileira de Pagamento Instantaneo