API e sandbox
O seu sistema (ERP, PDV, core bancário, e-commerce) avisa a Promotech de cada compra e o participante recebe na hora os números da sorte (Promo-fácil) ou as chances para jogar (Prêmio Instantâneo). Uma compra por chamada, em JSON, com a mesma regra da importação por arquivo. Para testar sem risco, use a chave de teste: mesma URL e mesmas regras, com gravação separada da campanha.
Visão geral
| Promo-fácil | Prêmio Instantâneo | |
|---|---|---|
| O que a compra gera | números da sorte (sorteio pela Loteria Federal) | chances para jogar no site da promoção |
| URL base | https://promotech.com.br/promo-facil/api/v1 | https://promotech.com.br/premioinstantaneo/api/v1 |
| Chave de produção | pf_ + 48 caracteres | pi_ + 48 caracteres |
| Chave de teste | pf_test_ + 48 caracteres | pi_test_ + 48 caracteres |
| Quem participa | CPF ou CNPJ | CPF |
| Disponível | planos de 10 séries ou mais | a partir do plano de 1 milhão de chances |
- Tudo é HTTPS, corpo em JSON UTF-8 (
Content-Type: application/json), datas emAAAA-MM-DDe valores com ponto decimal (89.90). - A chave diz qual campanha: você nunca informa a campanha, então não há como gravar na promoção errada. Uma chave por canal, um canal por campanha.
- Toda resposta tem
"ok": trueou"ok": falsee, no erro, um código estável em"erro"e um texto em"mensagem". Programe contra o código, não contra o texto. - A API aplica as mesmas regras do arquivo e do SFTP (período, lojas, limites por CPF e por dia, valor mínimo, impedidos). A mesma compra dá o mesmo resultado por qualquer canal.
Autenticação
A chave é gerada no painel da promoção, em Canais de entrada → canal do tipo API. Ela aparece uma única vez: guardamos só uma impressão digital (SHA-256), então nem a Promotech consegue mostrá-la de novo. Perdeu? Gere outra — a anterior para de valer na hora.
Authorization: Bearer pf_3f9c… (recomendado)
X-Api-Key: pf_3f9c… (alternativa) Chave desconhecida, de canal desativado ou ausente → 401. Guarde a chave como segredo do servidor: ela não deve ir para o navegador nem para o aplicativo do cliente final.
Sandbox (ambiente de teste)
Cada canal API pode ter, além da chave de produção, uma chave de teste (pf_test_… / pi_test_…), gerada no mesmo lugar (botão do frasco, Gerar chave de teste). Ela usa a mesma URL e o mesmo código da produção. A única diferença é o destino da gravação: uma cópia oculta da campanha.
- Mesmas regras: tipo de regra, valor por número/chance, saldo, limites, lojas, sorteios e impedidos são copiados da campanha. As respostas são as que você vai ver em produção.
- Funciona antes de contratar: com a campanha em demonstração, antes do início ou já encerrada. O período da sandbox acompanha o calendário para que hoje sempre valha.
- Nada vaza: números fictícios (série
DEMO) e chances que ninguém joga; nada aparece nos relatórios, na apuração, para o participante ou para a chave de produção. - Limite por minuto próprio: testar carga na sandbox não consome a cota da produção.
- Recomeçar do zero: no painel, Limpar dados de teste e recriar apaga o que o teste gravou e copia de novo a configuração atual da campanha.
Toda resposta da sandbox traz "sandbox": true no corpo e o cabeçalho X-Promotech-Ambiente: sandbox (na produção: producao). Use isso para garantir que o ambiente certo está configurado antes de virar a chave.
HTTP/1.1 201 Created
X-Promotech-Ambiente: sandbox
{ "ok": true, "sandbox": true, "transacao": { … } } Prêmio Instantâneo: na sandbox todo CPF participa (modelo automático), para você ver as chances nascerem. Se a campanha real for "só após o cadastro", em produção uma compra de quem ainda não se cadastrou volta com "aceito": false e motivo SEM_CADASTRO — trate esse caso.
Limite por minuto
Cada chave tem um limite de requisições por minuto (padrão 120, ajustável no canal). As respostas trazem:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 42 (segundos até a janela virar) Passou do limite → 429 limite_excedido com Retry-After em segundos. Espere esse tempo e repita a mesma chamada — com id_externo ela é segura (ver abaixo). Volume grande de uma vez (carga inicial, fechamento do dia)? Use a importação por arquivo ou SFTP, que processa centenas de milhares de linhas num lote só.
Idempotência: id_externo
Mande sempre o identificador da compra no seu sistema (número do pedido, NSU, id da transação) em id_externo, com até 80 caracteres. Repetir a chamada com o mesmo id_externo não gera números nem chances de novo: devolve 200 dizendo que já foi processada. Isso torna seguro repetir depois de um timeout ou de uma queda de rede — na dúvida, repita.
- Promo-fácil: recomendado (sem ele, cada chamada é uma compra nova). Repetição →
"repetida": true. - Prêmio Instantâneo: obrigatório. Repetição →
"ja_processado": true.
Promo-fácil
Transação → números da sorte. URL base https://promotech.com.br/promo-facil/api/v1
POST/transacoes
Registra uma transação e devolve os números gerados, o sorteio de destino e o resumo do participante. O participante que ainda não existe é criado em pré-cadastro: quando ele entrar no site da promoção com o mesmo CPF/CNPJ, encontra os números.
| Campo | Tipo | Descrição | |
|---|---|---|---|
cpf_cnpj | texto | obrigatório | CPF (11) ou CNPJ (14), com ou sem máscara; zeros à esquerda recompostos. |
nome | texto | obrigatório | Nome do participante. |
valor_transacao | número | obrigatório | Valor da compra (89.90; "89,90" também é aceito). Não pode ser negativo. |
data_transacao | data | opcional | AAAA-MM-DD (também dd/mm/aaaa). Vazio = hoje. Define o sorteio de destino. |
id_externo | texto | recomendado | Até 80 caracteres. Ver idempotência. |
cnpj_loja | texto | depende | CNPJ da loja da compra. Obrigatório na prática quando o sorteio é só de algumas lojas. |
loja | texto | opcional | Código da loja no seu sistema (alternativa ao CNPJ, se cadastrado na campanha). |
email, telefone | texto | opcional | Contato do participante (o telefone permite que ele se identifique pelo WhatsApp da promoção). |
chave_nfe, nota, serie, cnpj_emissor | texto | opcional | Dados fiscais para a sua conferência (chave_nfe com 44 dígitos). |
curl -X POST https://promotech.com.br/promo-facil/api/v1/transacoes \
-H "Authorization: Bearer $PROMOTECH_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"id_externo": "PED-100231",
"cpf_cnpj": "123.456.789-09",
"nome": "Maria Souza",
"valor_transacao": 250.00,
"data_transacao": "2026-09-28",
"cnpj_loja": "12.345.678/0001-90"
}'<?php
$ch = curl_init('https://promotech.com.br/promo-facil/api/v1/transacoes');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PROMOTECH_CHAVE'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'id_externo' => 'PED-100231',
'cpf_cnpj' => '12345678909',
'nome' => 'Maria Souza',
'valor_transacao' => 250.00,
'data_transacao' => '2026-09-28',
'cnpj_loja' => '12345678000190',
]),
]);
$resp = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status === 429) { /* espere Retry-After e repita a MESMA chamada */ }
if ($resp['ok']) {
foreach ($resp['numeros'] as $n) {
echo $n['completo'], PHP_EOL; // ex.: 000412345
}
}// Node 18+ (fetch nativo). Nunca no navegador: a chave é segredo do servidor.
const resp = await fetch('https://promotech.com.br/promo-facil/api/v1/transacoes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PROMOTECH_CHAVE}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id_externo: 'PED-100231',
cpf_cnpj: '12345678909',
nome: 'Maria Souza',
valor_transacao: 250.0,
data_transacao: '2026-09-28',
cnpj_loja: '12345678000190',
}),
});
const dados = await resp.json();
console.log(resp.headers.get('X-Promotech-Ambiente'), dados.transacao.situacao);import os, requests
r = requests.post(
'https://promotech.com.br/promo-facil/api/v1/transacoes',
headers={'Authorization': f"Bearer {os.environ['PROMOTECH_CHAVE']}"},
json={
'id_externo': 'PED-100231',
'cpf_cnpj': '12345678909',
'nome': 'Maria Souza',
'valor_transacao': 250.00,
'data_transacao': '2026-09-28',
'cnpj_loja': '12345678000190',
},
timeout=20,
)
if r.status_code == 429:
espera = int(r.headers['Retry-After']) # e repita a MESMA chamada
d = r.json()
print(d['transacao']['situacao'], [n['completo'] for n in d.get('numeros', [])])Resposta — 201 (nova) ou 200 (repetida):
{
"ok": true,
"transacao": {
"id": 58213,
"id_externo": "PED-100231",
"repetida": false,
"data": "2026-09-28",
"valor": 250,
"cnpj_loja": "12345678000190",
"loja_reconhecida": true,
"sorteio": { "id": 12, "nome": "1º sorteio", "empurrado": false },
"numeros_gerados": 2,
"situacao": "numeros_gerados",
"motivo": null,
"lote": 931
},
"numeros": [
{ "serie": "0004", "numero": "12345", "completo": "000412345", "ficticio": false, "atribuido_em": "2026-09-28 12:00:00" },
{ "serie": "0017", "numero": "80211", "completo": "001780211", "ficticio": false, "atribuido_em": "2026-09-28 12:00:00" }
],
"participante": {
"id": 7781, "cpf_cnpj": "12345678909", "nome": "Maria Souza",
"cadastrado_no_site": false, "saldo": 50, "total_numeros": 2,
"total_transacoes": 1, "valor_total_transacoes": 250, "primeiro_registro": "…"
},
"avisos": [],
"tempo_ms": 142
} avisos traz o que não impediu a transação mas merece atenção: loja não reconhecida, data que foi para o próximo sorteio (empurrado), números cortados pelo teto por CPF.
GET/participantes/{cpf_ou_cnpj}
Dados do participante nesta promoção: resumo, todos os números (com o sorteio de cada um) e as transações registradas. Documento só com dígitos. Não encontrado → 404 participante_nao_encontrado.
curl https://promotech.com.br/promo-facil/api/v1/participantes/12345678909 \
-H "Authorization: Bearer $PROMOTECH_CHAVE" {
"ok": true,
"participante": { "id": 7781, "cpf_cnpj": "12345678909", "nome": "Maria Souza", "saldo": 50, "total_numeros": 2, … },
"numeros": [
{ "serie": "0004", "numero": "12345", "completo": "000412345", "ficticio": false,
"atribuido_em": "…", "transacao_id": 58213, "sorteio": { "id": 12, "nome": "1º sorteio" } }
],
"transacoes": [
{ "id": 58213, "id_externo": "PED-100231", "data": "…", "valor": 250, "numeros_gerados": 2,
"recebida_em": "…", "loja": { "cnpj": "12345678000190", "nome": "Loja Centro" } }
]
}
GET/status
Confere a ligação e devolve a configuração da campanha da chave: período, se está recebendo, a regra e o calendário de sorteios. Bom primeiro teste e bom para o seu sistema se autoconfigurar.
{
"ok": true,
"campanha": { "id": 83, "nome": "…", "slug": "…", "inicio": "2026-09-17", "fim": "2026-12-16",
"modo": "producao", "recebendo": true, "motivo_bloqueio": null },
"regra": { "tipo": "por_valor", "valor_por_regra": 100, "numeros_por_regra": 1, "saldo_cumulativo": true,
"valor_minimo_transacao": null, "max_transacoes_por_dia": null, "max_numeros_por_cpf": null },
"sorteios": [ { "id": 12, "nome": "1º sorteio", "participacao_inicio": "…", "participacao_fim": "…",
"loteria_federal": "…", "apurado": false, "abrangencia": "todas_as_lojas" } ],
"canal": { "id": 5, "nome": "ERP da rede" }
} modo: producao (contratada), demonstracao (ainda não contratada — números fictícios) ou sandbox (chave de teste).
Situações da transação
Transação fora da regra não é erro: ela é registrada (para a sua conferência e para não entrar de novo) e volta 201 com a situação e o motivo em texto.
numeros_gerados | Gerou números. |
saldo_acumulado | Regra por valor: ainda não fechou um número; o valor ficou no saldo para a próxima compra. |
sem_numero | Aceita, mas a regra não deu número. |
repetida | Mesmo id_externo de antes; nada foi gerado de novo. |
fora_da_regra_fora_periodo | Data fora do período de participação (ou no futuro). |
fora_da_regra_valor_minimo | Abaixo do valor mínimo do regulamento. |
fora_da_regra_limite_dia | CPF já atingiu o máximo de transações do dia. |
fora_da_regra_limite_cpf | CPF já atingiu o teto de números da promoção. |
fora_da_regra_impedido | Documento impedido de participar (regulamento). |
fora_da_regra_opt_out | Participante pediu para não participar. |
fora_da_regra_sem_sorteio | Nenhum sorteio aberto recebe esta compra (data ou loja). |
fora_da_regra_cpf_invalido | Documento inválido. |
fora_da_regra_pool_esgotado | Acabaram os números contratados. |
Prêmio Instantâneo
Compra → chances para jogar no site da promoção. URL base https://promotech.com.br/premioinstantaneo/api/v1
POST/movimento
Registra uma compra. A pessoa recebe as chances na conta e joga quando entrar no site — o sistema nunca joga por ela.
| Campo | Tipo | Descrição | |
|---|---|---|---|
id_externo | texto | obrigatório | Até 80 caracteres. Ver idempotência. |
cpf | texto | obrigatório | CPF do comprador (só pessoa física joga). |
data | data | obrigatório | AAAA-MM-DD (também dd/mm/aaaa). |
valor | número | obrigatório | Valor da compra, sem sinal negativo. |
nome | texto | recomendado | Nome do comprador. |
loja ou cnpj_loja | texto | recomendado | Código da loja no seu sistema ou CNPJ. O prêmio sai do estoque dessa loja; sem loja, do estoque geral. |
email, telefone | texto | opcional | Para avisar o participante das chances (e-mail e WhatsApp). |
chave_nfe, nota, serie, cnpj_emissor | texto | opcional | Dados fiscais para conferência (chave_nfe com 44 dígitos). |
curl -X POST https://promotech.com.br/premioinstantaneo/api/v1/movimento \
-H "Authorization: Bearer $PROMOTECH_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"id_externo": "NF-55120",
"cpf": "12345678909",
"nome": "Maria Souza",
"data": "2026-09-28",
"valor": 180.50,
"loja": "012"
}'<?php
$ch = curl_init('https://promotech.com.br/premioinstantaneo/api/v1/movimento');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PROMOTECH_CHAVE'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'id_externo' => 'NF-55120',
'cpf' => '12345678909',
'nome' => 'Maria Souza',
'data' => '2026-09-28',
'valor' => 180.50,
'loja' => '012',
]),
]);
$resp = json_decode(curl_exec($ch), true);
if ($resp['ok'] && !empty($resp['aceito'])) {
echo $resp['chances'], ' chance(s) para jogar', PHP_EOL;
}const resp = await fetch('https://promotech.com.br/premioinstantaneo/api/v1/movimento', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PROMOTECH_CHAVE}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id_externo: 'NF-55120', cpf: '12345678909', nome: 'Maria Souza',
data: '2026-09-28', valor: 180.5, loja: '012',
}),
});
const d = await resp.json();
if (d.ja_processado) console.log('já registrada antes');
else console.log(d.aceito ? `${d.chances} chance(s)` : `recusada: ${d.motivo}`);import os, requests
r = requests.post(
'https://promotech.com.br/premioinstantaneo/api/v1/movimento',
headers={'Authorization': f"Bearer {os.environ['PROMOTECH_CHAVE']}"},
json={'id_externo': 'NF-55120', 'cpf': '12345678909', 'nome': 'Maria Souza',
'data': '2026-09-28', 'valor': 180.50, 'loja': '012'},
timeout=20,
)
d = r.json()
print(d.get('aceito'), d.get('chances'), d.get('motivo'))Resposta — 201 quando aceita; 200 quando registrada sem chance ou já processada:
{
"ok": true,
"id_externo": "NF-55120",
"aceito": true,
"motivo": null,
"chances": 1,
"cortadas": 0,
"participante": {
"cpf": "12345678909", "nome": "Maria Souza",
"situacao": "importado", // "ativado" depois que ele entra no site
"opt_out": false,
"chances": { "total": 1, "a_jogar": 1, "jogadas": 0, "canceladas": 0 },
"saldo": 30.5,
"movimento": [
{ "id": 90412, "id_externo": "NF-55120", "data": "2026-09-28", "valor": 180.5, "aceito": true,
"motivo": null, "chances": 1, "loja": "Loja Centro", "recebido_em": "2026-09-28 14:03:11" }
]
}
}
// repetição do mesmo id_externo:
{ "ok": true, "ja_processado": true, "id_externo": "NF-55120", "mensagem": "Este id_externo já participou. Nada foi alterado." } cortadas: chances que a regra daria mas o teto (por CPF ou do plano) não deixou emitir.
GET/participante/{cpf}
O mesmo bloco participante da resposta acima: situação, chances (total, a jogar, jogadas, canceladas), saldo e as últimas 20 compras (id, id_externo, data, valor, aceito, motivo, chances, loja, recebido_em) e as últimas 20 jogadas em jogadas (id, quando, premiado, premio, loja_retirada, entrega: PENDENTE, CONTATADO, ENTREGUE…) — é por aqui que o seu servidor confirma o que o widget avisou. CPF inválido → 422 cpf_invalido; não participa → 404 nao_encontrado.
curl https://promotech.com.br/premioinstantaneo/api/v1/participante/12345678909 \
-H "Authorization: Bearer $PROMOTECH_CHAVE"
GET/status
{
"ok": true,
"campanha": { "slug": "…", "nome": "…", "inicio": "…", "fim": "…", "no_ar": true,
"ambiente": "producao", "modelo_participacao": "AUTOMATICA" },
"regra": { "tipo": "VALOR", "valor_por_chance": 50, "chances_por_regra": 1, "usa_saldo": true,
"limite_cpf": null, "limite_dia": null },
"canal": { "nome": "ERP da rede", "limite_por_minuto": 120 },
"bloqueio": null
} modelo_participacao: AUTOMATICA (todo CPF da compra participa) ou APOS_CADASTRO (só compras a partir do dia em que a pessoa se cadastrou no site). bloqueio diz por que a campanha não está recebendo, quando não está.
Motivos de "aceito": false
SO_SALDO | Regra por valor: não fechou uma chance; o valor foi para o saldo. |
SEM_CADASTRO | Campanha "após cadastro" e o CPF ainda não se cadastrou no site. |
ANTES_DO_CADASTRO | Compra anterior à data do cadastro (campanha "após cadastro"). |
FORA_PERIODO | Data fora do período de participação (ou no futuro). |
DUPLICADO | id_externo repetido dentro do mesmo envio. |
LIMITE_DIA | CPF já atingiu o máximo de compras do dia. |
LIMITE_CPF | CPF já atingiu o teto de chances da promoção. |
LIMITE_PLANO | A promoção atingiu o total de chances contratado. |
OPT_OUT | Participante pediu para não participar. |
CPF_INVALIDO | CPF inválido. |
POST/widget/sessao widget do jogo
Coloca o jogo da promoção dentro do seu app ou site, com o participante já identificado: sem login, sem senha, sem sair do app. O sorteio continua no servidor da Promotech — o app só mostra.
- O cliente, logado no seu app, toca em "Jogar".
- O seu servidor chama
POST /widget/sessaocom o CPF e recebe umaurlde uso único, válida por 10 minutos. A chave nunca vai para o app. - O app abre essa URL numa WebView (ou aba do navegador, ou iframe). A tela mostra só o jogo — nada de menu ou links para fora.
- Na primeira vez do CPF, o widget pede o aceite do regulamento e do uso dos dados (exigência legal), ali mesmo. Depois, joga direto.
- Terminada a animação, o widget avisa o app (eventos abaixo) e oferece "Onde retirar", "Jogar outra chance" e "Fechar".
| Campo | Tipo | Descrição | |
|---|---|---|---|
cpf | texto | obrigatório | CPF do cliente logado no seu app. |
nome | texto | depende | Nome completo. Obrigatório para CPF ainda sem cadastro em promoção "após cadastro" — o aceite no widget cria o cadastro. |
email, telefone | texto | opcional | Gravados no cadastro no aceite, para avisos da promoção. |
retorno | texto | opcional | Para onde o botão "Fechar" leva quando o widget abre fora de WebView/iframe: https://… ou o esquema do seu app (meuapp://…). Precisa começar por um endereço cadastrado no canal (painel › Canais › Widget do jogo). |
curl -X POST https://promotech.com.br/premioinstantaneo/api/v1/widget/sessao \
-H "Authorization: Bearer $PROMOTECH_CHAVE" \
-H "Content-Type: application/json" \
-d '{
"cpf": "12345678909",
"nome": "Maria Souza",
"email": "maria@exemplo.com.br",
"retorno": "meuapp://promocao/fim"
}'<?php
// No SEU servidor, quando o cliente logado no app toca em "Jogar".
// A chave nunca vai para o app.
$ch = curl_init('https://promotech.com.br/premioinstantaneo/api/v1/widget/sessao');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PROMOTECH_CHAVE'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'cpf' => $cliente->cpf,
'nome' => $cliente->nome,
'retorno' => 'meuapp://promocao/fim',
]),
]);
$r = json_decode(curl_exec($ch), true);
if (!empty($r['ok'])) {
// devolva $r['url'] para o app abrir (vale 10 minutos, uma vez só)
echo json_encode(['url' => $r['url']]);
} else {
// ex.: nao_encontrado = CPF ainda sem compra nesta promoção
http_response_code(409);
echo json_encode(['erro' => $r['erro'], 'mensagem' => $r['mensagem']]);
}// Express: rota do SEU backend que o app chama para abrir o jogo
app.post('/promocao/jogar', exigeLogin, async (req, res) => {
const r = await fetch('https://promotech.com.br/premioinstantaneo/api/v1/widget/sessao', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PROMOTECH_CHAVE}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
cpf: req.user.cpf,
nome: req.user.nome,
retorno: 'meuapp://promocao/fim',
}),
});
const d = await r.json();
if (!d.ok) return res.status(409).json({ erro: d.erro, mensagem: d.mensagem });
res.json({ url: d.url, chances: d.participante.chances_a_jogar });
});import os, requests
r = requests.post(
'https://promotech.com.br/premioinstantaneo/api/v1/widget/sessao',
headers={'Authorization': f"Bearer {os.environ['PROMOTECH_CHAVE']}"},
json={'cpf': cliente.cpf, 'nome': cliente.nome, 'retorno': 'meuapp://promocao/fim'},
timeout=15,
)
d = r.json()
url = d['url'] if d.get('ok') else None # entregue ao app; vale 10 min, uma vezResposta
HTTP/1.1 201 Created
{
"ok": true,
"url": "https://promotech.com.br/premioinstantaneo/sua-promocao/widget/3f9c…(48 caracteres)",
"expira_em": "2026-09-28T18:52:10-03:00",
"expira_em_segundos": 600,
"uso_unico": true,
"participante": {
"situacao": "ativado", // importado | ativado | novo | teste
"chances_a_jogar": 2
}
} situacao: importado (tem compra, ainda não aceitou — o widget pede o aceite), ativado (joga direto), novo (promoção "após cadastro": o aceite cadastra e as compras valem a partir de então), teste (chave de teste). Uma URL por abertura: peça outra a cada toque em "Jogar".
| HTTP | erro | Quando |
|---|---|---|
| 404 | nao_encontrado | Promoção automática e o CPF ainda não tem compra enviada. Mostre "suas chances aparecem depois da próxima compra". |
| 409 | campanha_fora_do_ar, jogo_nao_configurado | Promoção fora do ar (use a chave de teste para integrar antes do início) ou ainda sem jogo escolhido. |
| 409 | opt_out, participante_inativo | O CPF pediu para não participar ou está inativo. |
| 422 | cpf_invalido, email_invalido, nome_obrigatorio, retorno_nao_permitido | Corrija o dado; para o retorno, cadastre o endereço no canal. |
Abrindo o widget
- WebView (recomendado em app): Android, iOS, React Native, Flutter. Ative JavaScript e cookies. Os eventos chegam pela ponte nativa, sem configuração no painel.
- Navegador do sistema (Custom Tabs / SFSafariViewController), para apps sem WebView: funciona igual; o app recebe o fim pelo
retorno(deep link) com?evento=fechar&jogada=…&premiado=S|N&chances=…. - Iframe em site: cadastre a origem do site no canal (
https://www.sualoja.com.br) e use o domínio próprio da promoção num subdomínio seu (ex.:promo.sualoja.com.br) — em domínio de terceiros o navegador bloqueia o cookie da sessão dentro do iframe.
// React Native (react-native-webview)
import { WebView } from 'react-native-webview';
<WebView
source={{ uri: urlDoWidget }} // a "url" que o seu backend recebeu
onMessage={(e) => {
const msg = JSON.parse(e.nativeEvent.data);
if (msg.fonte !== 'promotech-widget') return;
if (msg.evento === 'jogada') atualizarSaldoDeChances(msg.dados.chances_a_jogar);
if (msg.evento === 'fechar' || msg.evento === 'erro') navigation.goBack();
}}
/>// Android (Kotlin) — WebView
class PonteWidget(private val activity: Activity) {
@JavascriptInterface
fun postMessage(json: String) {
val msg = JSONObject(json)
when (msg.getString("evento")) {
"jogada" -> { /* msg.getJSONObject("dados").getBoolean("premiado") */ }
"fechar", "erro" -> activity.runOnUiThread { activity.finish() }
}
}
}
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
CookieManager.getInstance().setAcceptCookie(true)
webView.addJavascriptInterface(PonteWidget(this), "PromotechWidget")
webView.loadUrl(urlDoWidget)// iOS (Swift) — WKWebView
final class Ponte: NSObject, WKScriptMessageHandler {
func userContentController(_ c: WKUserContentController, didReceive m: WKScriptMessage) {
guard let msg = m.body as? [String: Any], let evento = msg["evento"] as? String else { return }
if evento == "fechar" || evento == "erro" { /* fechar a tela */ }
}
}
let config = WKWebViewConfiguration()
config.userContentController.add(Ponte(), name: "promotech")
let web = WKWebView(frame: .zero, configuration: config)
web.load(URLRequest(url: URL(string: urlDoWidget)!))// Flutter (flutter_inappwebview)
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(urlDoWidget)),
onWebViewCreated: (c) {
c.addJavaScriptHandler(handlerName: 'promotech', callback: (args) {
final msg = args.first as Map;
if (msg['evento'] == 'fechar' || msg['evento'] == 'erro') Navigator.pop(context);
});
},
)<!-- Site, com domínio próprio da promoção (ex.: promo.sualoja.com.br) -->
<iframe id="jogo" src="URL_DO_WIDGET" style="width:100%;height:640px;border:0" allow="vibrate"></iframe>
<script>
window.addEventListener('message', function (e) {
if (e.origin !== 'https://promo.sualoja.com.br') return; // a origem do widget
var msg = e.data;
if (!msg || msg.fonte !== 'promotech-widget') return;
if (msg.evento === 'fechar') document.getElementById('jogo').remove();
});
</script>Eventos
Mensagem: {"fonte": "promotech-widget", "versao": 1, "evento": "…", "teste": false, "dados": {…}}, entregue por ReactNativeWebView.postMessage, webkit.messageHandlers.promotech, a interface Android PromotechWidget, flutter_inappwebview.callHandler('promotech') ou postMessage para as origens cadastradas (iframe).
| evento | dados | Quando |
|---|---|---|
pronto | chances_a_jogar ou aceite_pendente | A tela abriu. |
jogada | jogada, premiado, premio, chances_a_jogar | Só depois da revelação na tela — o app nunca sabe o resultado antes do participante. |
sem_chances | codigo | Não há chance para jogar agora (ou acabou de se cadastrar). |
erro | codigo: link_expirado, sessao_encerrada, premios_esgotados, campanha_fora_do_ar… | Peça uma nova sessão ou feche. |
fechar | os últimos dados | O participante tocou em "Fechar". |
GET /participante/{cpf} (lista jogadas). Eventos e parâmetros do retorno servem à tela e podem ser forjados por quem controla o aparelho.pi_test_…, a sessão abre o jogo verdadeiro da promoção, com a faixa "Modo teste", resultado simulado e nada gravado — nem jogada, nem aceite, nem cadastro. Os eventos saem com "teste": true. Funciona antes do início da promoção.Erros
{ "ok": false, "erro": "dados_invalidos", "mensagem": "A transação tem campos inválidos.",
"campos": { "cpf_cnpj": "CPF inválido", "valor_transacao": "obrigatório (número, ex.: 89.90)" } } | HTTP | erro | O que fazer |
|---|---|---|
| 400 | json_invalido, corpo_vazio | Corpo não é JSON válido ou está vazio. Não repita sem corrigir. |
| 401 | nao_autenticado, chave_invalida | Chave ausente, errada ou canal desativado. |
| 403 | plano_sem_api | O plano contratado não inclui API. A chave de teste (sandbox) funciona em qualquer plano, e em demonstração a de produção também. |
| 404 | participante_nao_encontrado, nao_encontrado | Documento sem participação nesta promoção, ou rota inexistente. |
| 409 | campanha_fechada | Prêmio Instantâneo: a promoção não está recebendo (fora do ar ou período encerrado). |
| 422 | dados_invalidos (Promo-fácil, com campos), registro_invalido, cpf_invalido (Prêmio Instantâneo) | Corrija o dado. Não repita igual. |
| 429 | limite_excedido | Espere Retry-After segundos e repita. |
| 500 | falha_processamento, falha_ao_gravar | Nada foi gravado. Repita com o mesmo id_externo; persistindo, fale com o suporte. |
| 503 | campanha_indisponivel, sandbox_indisponivel | Promo-fácil fora do ar ou período encerrado; sandbox momentaneamente indisponível. Repita depois. |
Boas práticas
- Comece pela sandbox. Gere a chave de teste, chame
GET /status, depois envie compras reais do seu sistema. Confira o cabeçalhoX-Promotech-Ambiente. - Sempre com
id_externo, e repita em caso de timeout, 429, 500 ou 503 — nunca haverá duplicidade. - Chame depois que a venda estiver confirmada (paga/faturada). Não há estorno pela API: venda cancelada depois do envio precisa ser tratada com a equipe.
- Fila e retentativa do seu lado: se a Promotech estiver inacessível, guarde e reenvie — a data da compra vai no corpo, então o atraso não muda o sorteio de destino.
- Carga inicial e grandes volumes por arquivo ou SFTP, no painel; a API é para o dia a dia, compra a compra.
- Troca de chave: gere a nova, atualize o seu sistema e só então descarte a antiga — gerar derruba a anterior na hora.
Suporte à integração
Dúvida técnica, pedido de aumento do limite por minuto ou erro que persiste: abra um chamado ou escreva para contato@promotech.com.br. Mande o horário da chamada, o id_externo e o código do erro — nunca envie a chave; o prefixo (pf_test_ab12…) basta para identificarmos o canal.
Conheça os produtos: Promo-fácil · Prêmio Instantâneo · todos os sistemas.