개발자 문서

노티페이는 입금 SMS를 파싱해 사용자가 지정한 엔드포인트로 JSON을 POST합니다.
단순한 웹훅 구조이므로 어떤 서버 환경에서도 5분 안에 연동할 수 있습니다.

빠른 시작 (3단계)

1. 연동 서버 준비

JSON POST를 받을 URL 하나를 준비합니다. 자체 서버 / AWS Lambda / Cloudflare Workers / Vercel 등 무엇이든 OK.

2. 앱에 URL 등록

노티페이 앱 → 메인 화면 → Webhook URL 카드에 준비한 URL 입력 → 저장.

3. 실시간 수신

은행 입금 SMS가 도착하면 자동으로 등록한 URL로 JSON 페이로드가 전송됩니다.

API 명세

Method POST
URL 사용자가 앱에서 설정한 엔드포인트 URL
Content-Type application/json; charset=utf-8
Timeout 30초 (앱 측)
재시도 정책 실패 시 자동 재시도 — 최대 8회 (Exponential backoff: 30s · 60s · 2m · 4m · 8m · 16m · 32m, 약 1시간)

요청 본문 (Request Body)

{
  "deposit_datetime": "06/02 11:35",
  "depositor_name": "홍길동",
  "amount": "70000"
}
필드타입설명예시
deposit_datetime string 은행 SMS에 기재된 입금 일시 (은행마다 형식 다름) "06/02 11:35"
depositor_name string 입금자명 (한글/영문 모두) "홍길동"
amount string 입금 금액 — 콤마/원 표시 제거된 순수 숫자 문자열 "70000"

응답 (Response)

서버는 처리 결과를 JSON으로 반환해야 하며, HTTP 상태 코드 200 + body의 status 필드가 "success"일 때만 성공으로 간주합니다.

✓ 성공 (HTTP 200)
{
  "status": "success",
  "message": "Data received successfully."
}
✗ 실패 (HTTP 4xx, 5xx 또는 status≠"success")
{
  "status": "error",
  "message": "Missing required data."
}
중요: 응답이 {"status":"success"}가 아니거나 200 외 상태 코드면 앱이 자동으로 재시도합니다. 엔드포인트는 멱등(idempotent)해야 합니다 — 같은 데이터가 여러 번 도착해도 중복 처리되지 않도록 설계하세요.

빠른 테스트

1) 무료 테스트 사이트로 즉시 확인

webhook.site 접속 → "Your unique URL" 복사 → 앱에 붙여넣고 저장. 입금 SMS 받으면 그 사이트에서 JSON이 도착하는 것을 실시간으로 확인 가능합니다.

2) 직접 curl로 테스트

curl -X POST https://your-server.example.com/webhook \
  -H "Content-Type: application/json" \
  -d '{"deposit_datetime":"06/02 11:35","depositor_name":"홍길동","amount":"70000"}'

언어별 수신 예제

아래는 노티페이가 보낸 JSON을 받아서 처리하는 최소 예제입니다.

PHP

<?php
header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['status' => 'error', 'message' => 'POST only.']);
    exit;
}

// JSON Body 파싱
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);

if (!is_array($data)
    || !isset($data['deposit_datetime'], $data['depositor_name'], $data['amount'])) {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'message' => 'Missing required data.']);
    exit;
}

// === 여기서 본인 비즈니스 로직 처리 ===
//   $data['deposit_datetime']  : "06/02 11:35"
//   $data['depositor_name']    : "홍길동"
//   $data['amount']            : "70000"  (string, 숫자만)
// 예: DB 저장, 슬랙 알림, ERP 연동 등
// ====================================

http_response_code(200);
echo json_encode(['status' => 'success', 'message' => 'OK']);

Node.js (Express)

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

app.post('/webhook', (req, res) => {
    const { deposit_datetime, depositor_name, amount } = req.body || {};

    if (!deposit_datetime || !depositor_name || !amount) {
        return res.status(400).json({ status: 'error', message: 'Missing required data.' });
    }

    // 비즈니스 로직 (예: DB 저장)
    console.log(`입금: ${depositor_name} ${amount}원 @ ${deposit_datetime}`);

    res.status(200).json({ status: 'success', message: 'OK' });
});

app.listen(3000, () => console.log('listening on 3000'));

Python (표준 라이브러리)

import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        try:
            data = json.loads(self.rfile.read(length).decode('utf-8'))
        except json.JSONDecodeError:
            self._reply(400, {'status': 'error', 'message': 'Invalid JSON.'})
            return

        required = ('deposit_datetime', 'depositor_name', 'amount')
        if not all(k in data for k in required):
            self._reply(400, {'status': 'error', 'message': 'Missing required data.'})
            return

        # 비즈니스 로직
        print(f"입금: {data['depositor_name']} {data['amount']}원")

        self._reply(200, {'status': 'success', 'message': 'OK'})

    def _reply(self, code, body):
        self.send_response(code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.end_headers()
        self.wfile.write(json.dumps(body).encode('utf-8'))

HTTPServer(('', 8000), Handler).serve_forever()

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.get_json(silent=True) or {}
    if not all(k in data for k in ('deposit_datetime', 'depositor_name', 'amount')):
        return jsonify(status='error', message='Missing required data.'), 400

    # 비즈니스 로직
    print(f"입금: {data['depositor_name']} {data['amount']}원")

    return jsonify(status='success', message='OK'), 200

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

Java (Spring Boot)

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
public class WebhookController {

    @PostMapping("/webhook")
    public ResponseEntity<Map<String, String>> receive(@RequestBody Map<String, String> body) {
        String dt   = body.get("deposit_datetime");
        String name = body.get("depositor_name");
        String amt  = body.get("amount");

        if (dt == null || name == null || amt == null) {
            return ResponseEntity.badRequest().body(Map.of(
                "status", "error", "message", "Missing required data."
            ));
        }

        // 비즈니스 로직
        System.out.printf("입금: %s %s원%n", name, amt);

        return ResponseEntity.ok(Map.of("status", "success", "message", "OK"));
    }
}

C# (ASP.NET Core)

using Microsoft.AspNetCore.Mvc;

public class DepositPayload {
    public string deposit_datetime { get; set; }
    public string depositor_name   { get; set; }
    public string amount           { get; set; }
}

[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase {
    [HttpPost]
    public IActionResult Receive([FromBody] DepositPayload p) {
        if (string.IsNullOrEmpty(p?.deposit_datetime)
            || string.IsNullOrEmpty(p.depositor_name)
            || string.IsNullOrEmpty(p.amount))
            return BadRequest(new { status = "error", message = "Missing required data." });

        // 비즈니스 로직
        Console.WriteLine($"입금: {p.depositor_name} {p.amount}원");

        return Ok(new { status = "success", message = "OK" });
    }
}

Go

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type Payload struct {
    DepositDatetime string `json:"deposit_datetime"`
    DepositorName   string `json:"depositor_name"`
    Amount          string `json:"amount"`
}
type Resp struct {
    Status  string `json:"status"`
    Message string `json:"message"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    if r.Method != http.MethodPost {
        w.WriteHeader(http.StatusMethodNotAllowed)
        json.NewEncoder(w).Encode(Resp{"error", "POST only."})
        return
    }

    var p Payload
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil ||
        p.DepositDatetime == "" || p.DepositorName == "" || p.Amount == "" {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(Resp{"error", "Missing required data."})
        return
    }

    // 비즈니스 로직
    fmt.Printf("입금: %s %s원\n", p.DepositorName, p.Amount)

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(Resp{"success", "OK"})
}

func main() {
    http.HandleFunc("/webhook", handler)
    http.ListenAndServe(":8080", nil)
}

Ruby (Sinatra)

require 'sinatra'
require 'json'

post '/webhook' do
  content_type :json
  data = JSON.parse(request.body.read) rescue {}

  unless %w[deposit_datetime depositor_name amount].all? { |k| data.key?(k) }
    status 400
    return { status: 'error', message: 'Missing required data.' }.to_json
  end

  # 비즈니스 로직
  puts "입금: #{data['depositor_name']} #{data['amount']}원"

  status 200
  { status: 'success', message: 'OK' }.to_json
end

JSP (Java)

<%@ page contentType="application/json; charset=UTF-8" %>
<%@ page import="java.io.*, org.json.JSONObject" %>
<%
    if (!"POST".equalsIgnoreCase(request.getMethod())) {
        response.setStatus(405);
        out.print(new JSONObject().put("status","error").put("message","POST only."));
        return;
    }

    StringBuilder sb = new StringBuilder();
    try (BufferedReader br = request.getReader()) {
        String line; while ((line = br.readLine()) != null) sb.append(line);
    }
    JSONObject data = new JSONObject(sb.toString());

    if (!data.has("deposit_datetime") || !data.has("depositor_name") || !data.has("amount")) {
        response.setStatus(400);
        out.print(new JSONObject().put("status","error").put("message","Missing required data."));
        return;
    }

    // 비즈니스 로직
    System.out.println("입금: " + data.getString("depositor_name") + " " + data.getString("amount") + "원");

    response.setStatus(200);
    out.print(new JSONObject().put("status","success").put("message","OK"));
%>

모범 사례 (Best Practices)

  • HTTPS 사용 — HTTP는 권장하지 않습니다. SSL 무료 발급(Let's Encrypt) 활용.
  • 중복 처리 방지 — 같은 입금이 여러 번 들어와도 한 번만 처리하도록 (예: depositor + amount + datetime 조합으로 dedupe key 생성).
  • 응답은 즉시 — 비즈니스 로직을 큐에 넣고 응답은 바로 200 반환 (앱 timeout 30초). 무거운 처리는 백그라운드에서.
  • 로그 보관 — 받은 raw payload를 일정 기간 저장하면 디버깅에 도움됩니다.
  • 인증 추가 — 외부에 URL이 노출되므로 secret token을 URL 쿼리스트링이나 Header로 추가하면 안전합니다 (예: ?token=abc123).

자주 묻는 질문

Q. 응답을 200으로 안 보내면 어떻게 되나요?
앱이 자동으로 재시도합니다. 30초 → 60초 → 2분 → 4분 ... 총 8회, 약 1시간에 걸쳐 시도 후 영구 실패로 기록됩니다.
Q. 같은 입금이 두 번 도착할 수 있나요?
드물게 가능합니다. 안드로이드 시스템이 같은 SMS broadcast를 두 번 전달하거나, 재시도 중 응답이 늦게 도착해 중복 처리될 수 있습니다. 엔드포인트는 멱등(idempotent)하게 설계하세요. depositor_name + amount + deposit_datetime를 해시로 만들어 중복 키로 사용하는 방법이 일반적입니다.
Q. amount가 number가 아니라 string인 이유?
앱에서 SMS 텍스트를 그대로 정규식으로 추출해 콤마/원 표시만 제거한 형태로 전송합니다. 형변환은 수신측 에서 처리바랍니다. PHP (int)$data['amount'], JavaScript parseInt(amount, 10) 식으로 변환하세요.
Q. deposit_datetime의 포맷이 일정한가요?
은행마다 다릅니다 — KB/우리/농협: "06/02 11:35", 신한: "06/02 11:35:42". 앱은 SMS에 적힌 그대로 전달하며 ISO 8601 등으로 정규화하지 않습니다. 엄격한 datetime 포맷이 필요하면 수신측에서 파싱·재포맷하세요.
Q. SMS 본문 원본도 받고 싶어요.
현재 페이로드에는 원본이 포함되지 않습니다. 필요하시면 요청 주세요 — 옵션으로 추가 가능합니다.
Q. 로컬 개발 환경(localhost)으로 받을 수 있나요?
직접 접근 불가능합니다. 다음 방법 중 하나를 사용하세요:
  • ngrok 같은 터널링 도구로 로컬을 공인 URL로 노출
  • webhook.site에 페이로드 받아 확인 후 로컬에서 테스트 코드 실행
  • Cloudflare Tunnel, localtunnel 등