Pular para o conteúdo principal

Guia do Gateway USSD

← Voltar para a Documentação Principal

Este guia cobre o Gateway USSD do OmniSS7, que conecta diálogos USSD SS7/MAP a callbacks HTTP/JSON, permitindo que desenvolvedores de terceiros construam aplicações USSD com um simples endpoint HTTP.

Índice

  1. Visão Geral
  2. Arquitetura
  3. Habilitando o Gateway USSD
  4. Configuração
  5. Protocolo de Callback HTTP
  6. USSD Originado na Rede (Push API)
  7. Ciclo de Vida da Sessão
  8. Tratamento de Erros
  9. Métricas e Monitoramento
  10. Servidor de Callback de Exemplo
  11. Solução de Problemas

Visão Geral

O Gateway USSD lida com duas direções de tráfego USSD:

  • Originado pelo Móvel (Entrada) — Um assinante disca um código curto (por exemplo, *100#). O gateway recebe o processUnstructuredSS-Request MAP (opcode 59), o encaminha para seu callback HTTP e retransmite sua resposta de volta via SS7.
  • Originado na Rede (Saída) — Sua aplicação envia uma mensagem USSD para um assinante via a API REST. O gateway envia um unstructuredSS-Request MAP (opcode 60) via SS7 e roteia a resposta do assinante para seu callback.

Ambas as direções suportam diálogos de múltiplas interações — menus interativos onde o assinante responde e recebe prompts de acompanhamento.

Características Principais

PropriedadeValor
TransporteHTTP POST síncrono por interação
CodificaçãoAlfabeto padrão GSM de 7 bits (DCS 0x0F) conforme 3GPP TS 23.038
Comprimento máximo do texto182 caracteres (configurável)
Rastreamento de sessãoUUID gerado pelo gateway por diálogo
AutenticaçãoNenhuma (confia na rede SS7)
RoteamentoCorrespondência de prefixo de código curto aos URLs de callback

Referências 3GPP

EspecificaçãoRelevância
3GPP TS 23.090USSD Fase 2 — arquitetura e procedimentos
3GPP TS 24.090USSD Fase 3 — detalhes do protocolo
3GPP TS 29.002Protocolo MAP — USSD-Arg, USSD-Res, opcodes 59/60/61
3GPP TS 23.038Alfabeto padrão GSM de 7 bits e esquema de codificação de dados

Arquitetura

Fluxo Originado pelo Móvel (Entrada)

Fluxo Originado na Rede (Push Saída)

Visão Geral dos Componentes


Habilitando o Gateway USSD

O Gateway USSD requer que o modo Cliente MAP esteja habilitado, além de sua própria flag de recurso.

config :omniss7,
map_client_enabled: true,
ussd_gateway_enabled: true

O gateway também requer uma conexão M3UA funcional (veja Guia do Cliente MAP para configuração do M3UA).


Configuração

Parâmetros do Gateway USSD

config :omniss7,
ussd_gateway_enabled: true,
ussd_gateway: %{
# Roteamento de código curto — correspondência de prefixo mais longo
routes: [
%{pattern: "*100", url: "http://balance-app:9000/ussd"},
%{pattern: "*200", url: "http://topup-app:9000/ussd"},
%{pattern: "*", url: "http://default-app:9000/ussd"}
],

# Timeouts de sessão
session_timeout_ms: 180_000, # Duração total da sessão (3 minutos)
turn_timeout_ms: 30_000, # Tempo máximo de espera pela resposta do assinante por interação (30 segundos)

# Configurações de callback HTTP
http_timeout_ms: 5_000, # Timeout para HTTP POST para seu aplicativo (5 segundos)

# Limites de texto
max_text_length: 182 # Máximo de 7 bits GSM (trunca com aviso se excedido)
}

Referência de Parâmetros

ParâmetroTipoObrigatórioPadrãoDescrição
ussd_gateway_enabledBooleanoSimfalseInterruptor mestre para o recurso Gateway USSD
ussd_gateway.routesLista de MapasSim[]Regras de roteamento de prefixo de código curto. Cada entrada tem pattern (string de prefixo) e url (URL de callback). A correspondência de prefixo mais longo vence.
ussd_gateway.session_timeout_msInteiroNão180_000Duração total máxima da sessão em milissegundos. A sessão é encerrada com um erro se excedida.
ussd_gateway.turn_timeout_msInteiroNão30_000Tempo máximo para esperar pela resposta do assinante em um diálogo de múltiplas interações, em milissegundos.
ussd_gateway.http_timeout_msInteiroNão5_000Timeout de requisição HTTP para callbacks para sua aplicação, em milissegundos. Cobre tanto o tempo de conexão quanto o tempo de resposta.
ussd_gateway.max_text_lengthInteiroNão182Máximo de caracteres em uma string de texto USSD. Textos que excedem isso são truncados e um aviso é registrado.

Parâmetros de Roteamento

Cada entrada na lista routes é um mapa:

ParâmetroTipoObrigatórioDescrição
patternStringSimPrefixo de código curto a ser correspondido. Use "*" como um fallback abrangente. Prefixos mais longos têm prioridade.
urlStringSimURL do endpoint HTTP para receber POSTs de callback para códigos curtos correspondentes.

Correspondência de Roteamento

As rotas são correspondidas por prefixo mais longo primeiro. Para a string de discagem *100#:

  1. "*100" corresponde (comprimento 4) — selecionado
  2. "*10" corresponde (comprimento 3) — ignorado, mais curto
  3. "*" corresponde (comprimento 1) — fallback

Se nenhuma rota corresponder, o gateway retorna um erro MAP para o móvel e registra um aviso.


Protocolo de Callback HTTP

Sua aplicação recebe requisições HTTP POST do gateway e responde com instruções JSON.

Requisição do Gateway para Sua Aplicação

Content-Type: application/json

Primeira interação (iniciação da sessão):

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "initiation",
"text": "*100#",
"turn": 1
}

Interações subsequentes (assinante respondeu):

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "response",
"text": "1",
"turn": 2
}

Campos da Requisição

CampoTipoDescrição
session_idStringUUID gerado pelo gateway. Único por diálogo USSD. Use isso para correlacionar interações.
msisdnStringMSISDN do assinante (se disponível na mensagem MAP). Pode estar vazio para algumas redes.
typeString"initiation" para a primeira interação, "response" para respostas subsequentes do assinante.
textStringA string de discagem (por exemplo, *100#) na iniciação, ou a entrada do assinante (por exemplo, 1) na resposta.
turnInteiroContador de interações começando em 1. Incrementa com cada interação do assinante.

Resposta da Sua Aplicação

Sua aplicação deve responder com JSON contendo uma action e text:

Continuar (mostrar menu, esperar pela entrada do assinante):

{
"action": "continue",
"text": "1. Saldo\n2. Recarregar\n3. Transferir"
}

Encerrar (mostrar mensagem final, fechar sessão):

{
"action": "end",
"text": "Seu saldo é $5.00"
}

Campos da Resposta

CampoTipoObrigatórioDescrição
actionStringSim"continue" para manter a sessão aberta e esperar pela entrada do assinante, ou "end" para exibir uma mensagem final e fechar a sessão.
textStringSimTexto a ser exibido no dispositivo do assinante. Comprimento máximo regido por max_text_length (padrão 182). Use \n para quebras de linha.

USSD Originado na Rede (Push API)

Envie uma mensagem USSD para um assinante a partir de sua aplicação.

Endpoint

POST /api/ussd/send

Requisição

{
"msisdn": "+254712345678",
"text": "Você tem uma fatura pendente. Responda 1 para pagar.",
"callback_url": "http://billing-app:9000/ussd"
}

Campos da Requisição

CampoTipoObrigatórioDescrição
msisdnStringSimMSISDN do assinante de destino no formato internacional.
textStringSimTexto inicial USSD a ser exibido. Codificado como GSM de 7 bits.
callback_urlStringSimURL para receber a resposta do assinante via o protocolo de callback padrão.

Resposta

Sucesso (200 OK):

{
"session_id": "xyz-789-abc-123",
"status": "sent"
}

Respostas de erro:

Status HTTPCorpoCausa
400{"error": "invalid request", "required": ["msisdn", "text", "callback_url"]}Faltando um ou mais campos obrigatórios, ou o corpo não é um JSON válido
500{"error": "{:gsm7_encode_failed, ...}"}O texto contém caracteres que não estão no alfabeto GSM de 7 bits. A falha de codificação é apresentada através do ramo de erro genérico, então o status é 500, não 400.
500{"error": "..."}Qualquer outra falha de envio (por exemplo, conectividade M3UA). O campo error contém a razão interna inspecionada.
503{"error": "USSD gateway not enabled"}ussd_gateway_enabled é false

Nota sobre erros de validação vs. envio: Problemas de validação de campo (chaves faltando, JSON malformado) retornam 400 invalid request. Uma vez que a validação passa, qualquer falha do caminho de envio — incluindo falhas de codificação GSM de 7 bits — é relatada como 500 com a razão inspecionada no campo error.

Exemplo cURL

curl -X POST http://localhost:8080/api/ussd/send \
-H "Content-Type: application/json" \
-d '{
"msisdn": "+254712345678",
"text": "Você tem uma fatura pendente. Responda 1 para pagar.",
"callback_url": "http://billing-app:9000/ussd"
}'

Ciclo de Vida da Sessão

Cada diálogo USSD é rastreado como uma sessão com um session_id único.

Estados da Sessão

Sessão de Uma Interação

Se seu callback retornar "end" na primeira interação, nenhuma sessão persistente é criada. O gateway envia um MAP End com o resultado e retorna imediatamente.

Sessão de Múltiplas Interações

Se seu callback retornar "continue", o gateway:

  1. Cria um Session GenServer registrado em UssdGateway.Registry
  2. Envia um MAP Continue com opcode 60 (unstructuredSS-Request) para o móvel
  3. Espera pela resposta do assinante (até turn_timeout_ms)
  4. Encaminha a resposta para seu callback
  5. Repete até que seu callback retorne "end" ou ocorra um timeout

Comportamento de Timeout

TimeoutPadrãoEfeito
Timeout de interação30 segundosSe o assinante não responder dentro dessa janela, a sessão é encerrada com um erro MAP.
Timeout de sessão3 minutosDuração total da sessão. Encerra a sessão independentemente da atividade.
Timeout de callback HTTP5 segundosSe sua aplicação não responder a tempo, o gateway envia um erro MAP para o móvel e encerra a sessão.

Tratamento de Erros

O gateway lida com falhas de forma graciosa e sempre tenta enviar uma resposta de erro MAP para o móvel, para que o assinante veja uma mensagem significativa em vez de um timeout de rede.

CenárioAção do GatewayCódigo de Erro MAP
Timeout de callback HTTP ou 5xxEncerrar sessão, enviar MAP End com erro34 (systemFailure)
JSON inválido do callbackEncerrar sessão, enviar MAP End com erro34 (systemFailure)
Texto USSD excede max_text_lengthTruncar texto, registrar aviso, continuar normalmenteN/A (truncado, não é um erro)
Timeout do assinante (sem resposta)Encerrar sessão, enviar MAP End com erro34 (systemFailure)
Nenhuma rota corresponde ao código curtoEnviar MAP End com erro, registrar aviso34 (systemFailure)
Falha do Session GenServerSessão morre, assinante vê timeout de redeN/A (saída do processo)
Gateway USSD não habilitadoRetornar Facility Not Supported21 (facilityNotSupported)

Métricas e Monitoramento

O Gateway USSD expõe métricas Prometheus no endpoint padrão /metrics (porta 8080).

Métricas USSD

Métrica: ussd_requests_total
Tipo: Contador
Descrição: Total de requisições USSD processadas
Rótulos:

  • direction"inbound" (originado pelo móvel) ou "outbound" (push originado na rede)

Métrica: ussd_active_sessions
Tipo: Gauge
Descrição: Declarado como um gauge para o número de sessões USSD ativas.

Caveat: Este gauge é declarado, mas nunca atualizado na versão atual — ele sempre lê 0. Não confie nele para contagens de sessões ao vivo. Acompanhe a atividade da sessão via ussd_requests_total em vez disso.

Métrica: map_request_duration_milliseconds
Tipo: Histograma
Descrição: Duração das operações de envio USSD em milissegundos
Rótulos:

  • operation"ussd_send" para requisições de push de saída

Exemplos de Consultas Prometheus

# Taxa de requisições USSD por direção (entrada vs saída)
rate(ussd_requests_total[5m])

# Latência USSD de saída (p95)
histogram_quantile(0.95, rate(map_request_duration_milliseconds_bucket{operation="ussd_send"}[5m]))

Servidor de Callback de Exemplo

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)
sessions = {}

@app.route('/ussd', methods=['POST'])
def ussd():
data = request.json
session_id = data['session_id']
text = data['text']
turn = data['turn']

if data['type'] == 'initiation':
sessions[session_id] = {'state': 'main_menu'}
return jsonify({
'action': 'continue',
'text': 'Bem-vindo!\n1. Ver saldo\n2. Comprar crédito\n3. Transferir'
})

state = sessions.get(session_id, {}).get('state')

if state == 'main_menu':
if text == '1':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'Seu saldo é $5.00'
})
elif text == '2':
sessions[session_id]['state'] = 'buy_airtime'
return jsonify({
'action': 'continue',
'text': 'Digite o valor:'
})
else:
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'Opção inválida. Adeus.'
})

elif state == 'buy_airtime':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': f'Você comprou ${text} de crédito. Obrigado!'
})

return jsonify({'action': 'end', 'text': 'Sessão expirada.'})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)

Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

const sessions = new Map();

app.post('/ussd', (req, res) => {
const { session_id, text, type } = req.body;

if (type === 'initiation') {
sessions.set(session_id, { state: 'main_menu' });
return res.json({
action: 'continue',
text: 'Bem-vindo!\n1. Ver saldo\n2. Comprar crédito'
});
}

const session = sessions.get(session_id);
if (!session) {
return res.json({ action: 'end', text: 'Sessão expirada.' });
}

if (session.state === 'main_menu' && text === '1') {
sessions.delete(session_id);
return res.json({ action: 'end', text: 'Seu saldo é $5.00' });
}

sessions.delete(session_id);
return res.json({ action: 'end', text: 'Adeus.' });
});

app.listen(9000, () => console.log('Callback USSD na porta 9000'));

Solução de Problemas

O Discar USSD Retorna "Serviço Não Disponível"

Sintomas: O assinante disca um código curto e imediatamente recebe um erro de rede.

Possíveis causas:

  • ussd_gateway_enabled é false
  • Nenhuma rota corresponde ao código curto discado
  • Conexão M3UA está inativa

Resolução:

  1. Verifique ussd_gateway_enabled: true na configuração
  2. Verifique se um padrão de rota corresponde ao código curto (lembre-se de incluir * como catch-all)
  3. Verifique o status do par M3UA na interface web (página de Pares)

Callback Não Recebendo Requisições

Sintomas: Os logs do gateway mostram início USSD, mas sua aplicação nunca recebe o POST HTTP.

Possíveis causas:

  • URL de callback é inacessível a partir do host OmniSS7
  • Firewall bloqueando HTTP de saída do OmniSS7
  • Aplicação de callback não está em execução

Resolução:

  1. Teste a conectividade: curl -v http://your-app:9000/ussd a partir do host OmniSS7
  2. Verifique as regras do firewall para HTTP de saída
  3. Verifique se sua aplicação de callback está escutando na porta configurada

Sessões Expirando Prematuramente

Sintomas: Sessões de múltiplas interações terminam com "systemFailure" antes que o assinante possa responder.

Possíveis causas:

  • turn_timeout_ms é muito curto para sua base de assinantes
  • http_timeout_ms é muito curto para o tempo de processamento da sua aplicação
  • Latência de rede entre OmniSS7 e seu servidor de callback

Resolução:

  1. Aumente turn_timeout_ms (o padrão de 30 segundos deve ser suficiente para a maioria dos casos)
  2. Aumente http_timeout_ms se sua aplicação precisar de mais tempo de processamento
  3. Implemente o servidor de callback próximo ao OmniSS7 para reduzir a latência

Erros de Codificação GSM de 7 bits

Sintomas: Erros gsm7_encode_failed nos logs, ou uma resposta 500 de /api/ussd/send cujo campo error contém {:gsm7_encode_failed, ...}.

Possíveis causas:

  • O texto contém caracteres fora do alfabeto padrão GSM de 7 bits (por exemplo, emoji, caracteres CJK)

Resolução:

  • Restringir o texto USSD ao conjunto básico de caracteres GSM: letras ASCII, dígitos, pontuação comum e alguns caracteres gregos/nórdicos
  • Veja 3GPP TS 23.038 Seção 6.2.1 para a tabela completa de caracteres

Documentação Relacionada