Para desenvolvedores

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ácilPrêmio Instantâneo
O que a compra geranúmeros da sorte (sorteio pela Loteria Federal)chances para jogar no site da promoção
URL basehttps://promotech.com.br/promo-facil/api/v1https://promotech.com.br/premioinstantaneo/api/v1
Chave de produçãopf_ + 48 caracterespi_ + 48 caracteres
Chave de testepf_test_ + 48 caracterespi_test_ + 48 caracteres
Quem participaCPF ou CNPJCPF
Disponívelplanos de 10 séries ou maisa partir do plano de 1 milhão de chances
  • Tudo é HTTPS, corpo em JSON UTF-8 (Content-Type: application/json), datas em AAAA-MM-DD e 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": true ou "ok": false e, 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.

CampoTipoDescrição
cpf_cnpjtextoobrigatórioCPF (11) ou CNPJ (14), com ou sem máscara; zeros à esquerda recompostos.
nometextoobrigatórioNome do participante.
valor_transacaonúmeroobrigatórioValor da compra (89.90; "89,90" também é aceito). Não pode ser negativo.
data_transacaodataopcionalAAAA-MM-DD (também dd/mm/aaaa). Vazio = hoje. Define o sorteio de destino.
id_externotextorecomendadoAté 80 caracteres. Ver idempotência.
cnpj_lojatextodependeCNPJ da loja da compra. Obrigatório na prática quando o sorteio é só de algumas lojas.
lojatextoopcionalCódigo da loja no seu sistema (alternativa ao CNPJ, se cadastrado na campanha).
email, telefonetextoopcionalContato do participante (o telefone permite que ele se identifique pelo WhatsApp da promoção).
chave_nfe, nota, serie, cnpj_emissortextoopcionalDados 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_geradosGerou números.
saldo_acumuladoRegra por valor: ainda não fechou um número; o valor ficou no saldo para a próxima compra.
sem_numeroAceita, mas a regra não deu número.
repetidaMesmo id_externo de antes; nada foi gerado de novo.
fora_da_regra_fora_periodoData fora do período de participação (ou no futuro).
fora_da_regra_valor_minimoAbaixo do valor mínimo do regulamento.
fora_da_regra_limite_diaCPF já atingiu o máximo de transações do dia.
fora_da_regra_limite_cpfCPF já atingiu o teto de números da promoção.
fora_da_regra_impedidoDocumento impedido de participar (regulamento).
fora_da_regra_opt_outParticipante pediu para não participar.
fora_da_regra_sem_sorteioNenhum sorteio aberto recebe esta compra (data ou loja).
fora_da_regra_cpf_invalidoDocumento inválido.
fora_da_regra_pool_esgotadoAcabaram 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.

CampoTipoDescrição
id_externotextoobrigatórioAté 80 caracteres. Ver idempotência.
cpftextoobrigatórioCPF do comprador (só pessoa física joga).
datadataobrigatórioAAAA-MM-DD (também dd/mm/aaaa).
valornúmeroobrigatórioValor da compra, sem sinal negativo.
nometextorecomendadoNome do comprador.
loja ou cnpj_lojatextorecomendadoCódigo da loja no seu sistema ou CNPJ. O prêmio sai do estoque dessa loja; sem loja, do estoque geral.
email, telefonetextoopcionalPara avisar o participante das chances (e-mail e WhatsApp).
chave_nfe, nota, serie, cnpj_emissortextoopcionalDados 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_SALDORegra por valor: não fechou uma chance; o valor foi para o saldo.
SEM_CADASTROCampanha "após cadastro" e o CPF ainda não se cadastrou no site.
ANTES_DO_CADASTROCompra anterior à data do cadastro (campanha "após cadastro").
FORA_PERIODOData fora do período de participação (ou no futuro).
DUPLICADOid_externo repetido dentro do mesmo envio.
LIMITE_DIACPF já atingiu o máximo de compras do dia.
LIMITE_CPFCPF já atingiu o teto de chances da promoção.
LIMITE_PLANOA promoção atingiu o total de chances contratado.
OPT_OUTParticipante pediu para não participar.
CPF_INVALIDOCPF 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.

  1. O cliente, logado no seu app, toca em "Jogar".
  2. O seu servidor chama POST /widget/sessao com o CPF e recebe uma url de uso único, válida por 10 minutos. A chave nunca vai para o app.
  3. 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.
  4. 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.
  5. Terminada a animação, o widget avisa o app (eventos abaixo) e oferece "Onde retirar", "Jogar outra chance" e "Fechar".
CampoTipoDescrição
cpftextoobrigatórioCPF do cliente logado no seu app.
nometextodependeNome completo. Obrigatório para CPF ainda sem cadastro em promoção "após cadastro" — o aceite no widget cria o cadastro.
email, telefonetextoopcionalGravados no cadastro no aceite, para avisos da promoção.
retornotextoopcionalPara 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 vez

Resposta

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".

HTTPerroQuando
404nao_encontradoPromoção automática e o CPF ainda não tem compra enviada. Mostre "suas chances aparecem depois da próxima compra".
409campanha_fora_do_ar, jogo_nao_configuradoPromoção fora do ar (use a chave de teste para integrar antes do início) ou ainda sem jogo escolhido.
409opt_out, participante_inativoO CPF pediu para não participar ou está inativo.
422cpf_invalido, email_invalido, nome_obrigatorio, retorno_nao_permitidoCorrija 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).

eventodadosQuando
prontochances_a_jogar ou aceite_pendenteA tela abriu.
jogadajogada, premiado, premio, chances_a_jogarSó depois da revelação na tela — o app nunca sabe o resultado antes do participante.
sem_chancescodigoNão há chance para jogar agora (ou acabou de se cadastrar).
errocodigo: link_expirado, sessao_encerrada, premios_esgotados, campanha_fora_do_ar…Peça uma nova sessão ou feche.
fecharos últimos dadosO participante tocou em "Fechar".
Para dar benefício no app por causa de um prêmio, confirme no servidor com GET /participante/{cpf} (lista jogadas). Eventos e parâmetros do retorno servem à tela e podem ser forjados por quem controla o aparelho.
Modo teste: com a chave 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)" } }
HTTPerroO que fazer
400json_invalido, corpo_vazioCorpo não é JSON válido ou está vazio. Não repita sem corrigir.
401nao_autenticado, chave_invalidaChave ausente, errada ou canal desativado.
403plano_sem_apiO 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.
404participante_nao_encontrado, nao_encontradoDocumento sem participação nesta promoção, ou rota inexistente.
409campanha_fechadaPrêmio Instantâneo: a promoção não está recebendo (fora do ar ou período encerrado).
422dados_invalidos (Promo-fácil, com campos), registro_invalido, cpf_invalido (Prêmio Instantâneo)Corrija o dado. Não repita igual.
429limite_excedidoEspere Retry-After segundos e repita.
500falha_processamento, falha_ao_gravarNada foi gravado. Repita com o mesmo id_externo; persistindo, fale com o suporte.
503campanha_indisponivel, sandbox_indisponivelPromo-fácil fora do ar ou período encerrado; sandbox momentaneamente indisponível. Repita depois.

Boas práticas

  1. Comece pela sandbox. Gere a chave de teste, chame GET /status, depois envie compras reais do seu sistema. Confira o cabeçalho X-Promotech-Ambiente.
  2. Sempre com id_externo, e repita em caso de timeout, 429, 500 ou 503 — nunca haverá duplicidade.
  3. 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.
  4. 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.
  5. Carga inicial e grandes volumes por arquivo ou SFTP, no painel; a API é para o dia a dia, compra a compra.
  6. 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.