> ## Documentation Index
> Fetch the complete documentation index at: https://docs.meterengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 전송, 재전송, 중복 처리

> 웹훅 전송의 성공 기준과 시간 제한, 자동 재전송 일정, 실패한 전송을 다시 보내는 방법, 중복과 순서 문제를 다루는 방법을 설명합니다.

웹훅은 일이 생긴 순간 바로 나가지 않고 발송 대기에 쌓였다가 발송 작업이 보냅니다. 발송 작업은 30초 간격으로 돌며 한 번에 최대 100건을 보냅니다. 그래서 일이 생긴 뒤 수신 서버에 닿기까지 수십 초가 걸릴 수 있고, 대기 중인 전송이 많으면 더 늦어집니다.

## 성공과 실패의 기준

| 수신 서버의 반응 | MeterEngine의 판단 | 다시 보내는가 |
| - | - | - |
| `2xx` 응답 | 성공 | 아니요 |
| `3xx` 응답 | 실패. 리다이렉트를 따라가지 않습니다 | 예 |
| `4xx`, `5xx` 응답 | 실패 | 예 |
| 시간 제한 안에 연결되지 않거나 응답 상태가 오지 않음 | 실패 | 예 |
| 연결 거절, TLS 오류, 인증서 불일치 | 실패 | 예 |

응답 본문과 응답 헤더는 읽지 않습니다. 성공인지 실패인지는 상태 코드만으로 정합니다. 서명 검증에 실패한 요청에 `400`을 돌려주면 그 전송도 실패로 기록되어 다시 옵니다.

### 시간 제한

| 구간 | 제한 |
| - | - |
| TCP 연결 | 5초 |
| 요청을 다 보낸 뒤 응답 상태 줄이 올 때까지 | 10초 |
| 주소 확인, 연결, TLS, 요청, 응답을 합친 한 시도 전체 | 15초 |

> [!TIP]
>
> 수신 서버는 서명을 확인하고 중복을 거른 다음 바로 2xx로 응답하고, 실제 처리는 큐나 작업 테이블에 넘겨 나중에 하세요. 처리를 다 끝낸 뒤 응답하면 시간 제한에 걸려 같은 웹훅을 다시 받게 됩니다.

## 자동 재전송

실패하면 아래 간격을 두고 다시 보냅니다. 처음 시도를 포함해 모두 6번 시도합니다.

| 시도 | 앞 시도가 실패한 뒤 |
| - | - |
| 1번째 | (처음 보냄) |
| 2번째 | 1분 뒤 |
| 3번째 | 5분 뒤 |
| 4번째 | 30분 뒤 |
| 5번째 | 2시간 뒤 |
| 6번째 | 8시간 뒤 |

간격이 지난 뒤 다음 발송 작업이 보내므로 실제 간격은 표보다 조금 깁니다. 6번째 시도까지 실패하면 그 전송은 '발송 실패'(`failed`)로 끝나고 더는 자동으로 보내지 않습니다. 처음 시도에서 마지막 시도까지 10시간 남짓 걸리므로, 수신 서버를 그보다 오래 멈출 일이 있으면 다시 켠 뒤 실패한 전송을 수동으로 다시 보내야 합니다.

다음 두 경우는 횟수가 남아 있어도 바로 '발송 실패'로 끝납니다.

* 보낼 때 수신 주소가 공개되지 않은 주소로 풀린 경우(`blocked_address`)
* 수신 주소의 발송을 일시 중지했거나 보관한 경우(`endpoint_disabled`)

다시 보낼 때마다 `webhook-timestamp`와 `webhook-signature`는 새로 만들고, `webhook-id`와 본문은 처음과 같습니다.

## 실패 사유

전송 기록의 `failure_reason`과 콘솔 '실패 사유' 열의 값입니다.

| `failure_reason` | 콘솔 표기 | 뜻과 할 일 |
| - | - | - |
| `unexpected_status` | 성공이 아닌 응답 | 2xx가 아닌 상태 코드를 받았습니다. `response_status`에 받은 코드가 있습니다. 서명 검증 실패, 인증 미들웨어, 리다이렉트 설정을 확인하세요 |
| `timeout` | 응답 시간 초과 | 연결이나 응답 상태가 시간 제한을 넘었습니다. 수신 서버가 처리를 뒤로 미루고 먼저 응답하는지, 방화벽이 연결을 버리지 않는지 확인하세요 |
| `connection_error` | 연결 실패 | 연결 거절, DNS 오류, TLS 핸드셰이크 실패, 인증서와 호스트 이름 불일치가 모두 이 값으로 남습니다. 수신 서버가 떠 있는지, 인증서가 유효한지 확인하세요 |
| `blocked_address` | 허용되지 않는 주소 | 보내는 시점에 호스트 이름이 사설망이나 루프백 같은 공개되지 않은 주소로 풀렸습니다. 다시 보내지 않습니다. DNS 설정을 고친 뒤 수동으로 다시 보내세요 |
| `endpoint_disabled` | 발송을 멈춘 수신 주소 | 보내려 할 때 수신 주소가 일시 중지나 보관 상태였습니다. 다시 보내지 않습니다 |
| `lease_expired` | 발송 처리 시간 초과 | 마지막 시도의 결과를 MeterEngine이 기록하지 못한 채 끝났습니다. 수신 서버는 요청을 받았을 수도 있으니 `webhook-id`로 처리 여부를 확인한 뒤 필요하면 다시 보내세요 |
| `secret_unavailable` | 서명 비밀값 확인 불가 | MeterEngine 쪽에서 서명을 만들지 못했습니다. 횟수가 남았으면 자동으로 다시 보냅니다 |

`secret_unavailable` 실패가 계속되면 [MeterEngine 소개](https://docs.meterengine.com/index.md)의 문의 절에 적힌 방법으로 알려 주세요.

## 전송 기록 보기

콘솔에서는 'API 키와 통보' 화면에서 수신 주소의 '발송 내역과 설정'을 엽니다. '통보 발송 내역' 화면에 최근 것부터 종류, 상태, 시도 수, 수신 응답, 실패 사유, 다음 발송이 나옵니다.

| 콘솔 상태 | API `status` | 뜻 |
| - | - | - |
| 발송 대기 | `pending` (`attempt_count`가 0) | 아직 한 번도 보내지 않았습니다 |
| 재시도 대기 | `pending` (`attempt_count`가 1 이상) | 보냈지만 실패해 다음 시도를 기다리거나 지금 보내는 중입니다 |
| 발송 완료 | `done` | 2xx를 받았습니다 |
| 발송 실패 | `failed` | 더 보내지 않습니다. 수동으로 다시 보낼 수 있습니다 |

API로는 read 권한 키로 `GET /v1/webhook-endpoints/{id}/deliveries`를 부릅니다.

| 쿼리 | 설명 |
| - | - |
| `status` | `pending`, `done`, `failed` 가운데 하나. 생략하면 전부 |
| `page` | 0부터 시작하는 쪽 번호. 기본 `0` |
| `size` | 한 쪽의 건수. 1에서 100, 기본 `20` |

```bash curl theme={null}
curl "https://meterengine.com/v1/webhook-endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7/deliveries?status=failed&page=0&size=20" \
  -H "Authorization: Bearer me_test_xxxxxxxx"
```

```json 200 OK theme={null}
{
  "page": 0,
  "size": 20,
  "total": 1,
  "deliveries": [
    {
      "id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6e",
      "webhook_event_id": "6d1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a",
      "type": "payment_succeeded",
      "status": "failed",
      "attempt_count": 6,
      "next_attempt_at": null,
      "last_attempted_at": "2026-11-05T11:39:02.551930Z",
      "response_status": 500,
      "failure_reason": "unexpected_status",
      "created_at": "2026-11-05T01:20:00.481206Z",
      "attempts": [
        {
          "attempted_at": "2026-11-05T01:20:12.304118Z",
          "response_status": 500,
          "failure_reason": "unexpected_status",
          "duration_ms": 184
        }
      ]
    }
  ]
}
```

예시의 `attempts`는 줄였습니다. 실제로는 시도마다 한 항목이 오래된 것부터 실립니다. `webhook_event_id`는 수신 서버가 받은 `webhook-id` 헤더 값과 같으므로, 수신 서버 로그와 맞춰 볼 때 이 값을 씁니다. 전송 기록과 시도 기록을 지우는 API는 없고, 수신 주소를 보관해도 기록은 남습니다.

## 실패한 전송 다시 보내기

'발송 실패'로 끝난 전송만 다시 보낼 수 있습니다. 다시 보내면 시도 횟수가 처음부터 다시 세어져 위의 자동 재전송 일정이 처음부터 적용되고, 이전 시도 기록은 그대로 남습니다.

**1. 실패 원인을 고친다**

'실패 사유'와 '수신 응답'을 보고 수신 서버나 비밀값, DNS를 먼저 고칩니다. 고치지 않고 다시 보내면 같은 이유로 실패합니다.

**2. 수신 주소가 '발송 중'인지 확인한다**

일시 중지했거나 보관한 수신 주소의 전송은 다시 보낼 수 없습니다. 보관했다면 복원하고, '발송 상태'를 '발송 사용'으로 바꿉니다.

**3. 다시 보낸다**

콘솔에서는 '통보 발송 내역'의 해당 행에서 '다시 보내기'를 누릅니다. API로는 admin 권한 키로 `POST /v1/webhook-endpoints/{id}/deliveries/{delivery_id}/retries`를 부릅니다(본문 없음). 성공하면 `202 Accepted`와 빈 본문이 옵니다.

```bash curl theme={null}
curl -X POST https://meterengine.com/v1/webhook-endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7/deliveries/2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6e/retries \
  -H "Authorization: Bearer me_test_xxxxxxxx"
```

**4. 결과를 확인한다**

`202`는 발송 대기에 다시 넣었다는 뜻입니다. 잠시 뒤 전송 기록에서 '발송 완료'로 바뀌었는지 봅니다.

| 응답 | 뜻 |
| - | - |
| `202` | 다시 발송 대기에 넣었습니다 |
| `404 webhook_endpoint_not_found` | 이 업무 환경에 그런 수신 주소가 없습니다. 다른 환경의 키를 쓰지 않았는지 확인하세요 |
| `404 webhook_delivery_not_found` | 그 수신 주소에 그런 전송이 없습니다 |
| `409 webhook_delivery_not_retryable` | 전송이 '발송 실패'가 아니거나(아직 보내는 중이거나 이미 성공), 수신 주소가 일시 중지 또는 보관 상태입니다 |

다시 보낸 웹훅은 처음과 같은 `webhook-id`와 본문으로 갑니다. 수신 서버는 이미 처리한 웹훅을 한 번 더 받을 수 있다고 보고 아래처럼 중복을 걸러야 합니다.

## 일시 중지와 보관의 영향

> [!WARNING]
>
> 수신 주소의 발송을 일시 중지하거나 보관하면, 그동안 생긴 일은 그 수신 주소의 발송 대기에 아예 쌓이지 않습니다. 나중에 '발송 사용'으로 되돌리거나 복원해도 그 사이의 웹훅은 보내지 않고, 다시 보낼 전송 기록도 없습니다. 그 사이의 결제 결과는 API로 조회해 맞추세요.

* 일시 중지나 보관 전에 이미 대기 중이던 전송은 다음 시도 때 `endpoint_disabled`로 '발송 실패'가 됩니다. 이 전송은 '발송 사용'으로 되돌린 뒤 수동으로 다시 보낼 수 있습니다.
* 잠깐 수신 서버를 내려야 한다면 일시 중지하지 말고 그대로 두세요. 자동 재전송이 10시간 남짓 동안 다시 보내고, 그 뒤에 실패한 것은 수동으로 다시 보내면 됩니다.

## 중복 거르기

같은 웹훅이 두 번 이상 올 수 있습니다.

* 수신 서버는 처리했지만 응답이 시간 제한을 넘겨 MeterEngine이 실패로 보고 다시 보낸 경우
* 수신 서버의 응답이 네트워크에서 사라진 경우
* 수동으로 다시 보낸 경우
* 같은 URL을 수신 주소 둘로 등록한 경우(비밀값을 바꾸는 동안 등). 수신 주소가 여럿이어도 한 웹훅의 `webhook-id`는 같습니다

`webhook-id`는 웹훅 하나에 하나이고 다시 보내도 바뀌지 않습니다. 처리한 `webhook-id`를 저장하고, 이미 있는 값이면 처리하지 않고 2xx로 응답하세요.

**1. 처리 기록 테이블을 둔다**

`webhook-id`에 유일 제약을 건 테이블을 만듭니다.

**2. 처리와 기록을 한 트랜잭션에 묶는다**

`webhook-id`를 넣는 일과 도입사 시스템을 바꾸는 일을 같은 트랜잭션에서 합니다. 유일 제약 위반이 나면 이미 처리한 웹훅이니 2xx로 응답하고 끝냅니다.

`unbilled_usage_detected`는 매일 새 `webhook-id`로 오므로 이 방법으로는 반복이 걸러지지 않습니다. [웹훅 이벤트 유형과 data](https://docs.meterengine.com/webhooks/events.md)를 보세요.

## 순서

웹훅이 일이 생긴 순서대로 도착한다고 기대하지 마세요. 앞선 웹훅이 실패해 재전송을 기다리는 동안 뒤의 웹훅이 먼저 도착할 수 있습니다. 예를 들어 같은 인보이스의 `payment_failed`보다 다음 시도의 `payment_succeeded`가 먼저 올 수 있습니다.

* 본문의 `timestamp`는 웹훅을 만든 시각이라 순서를 짐작하는 데는 쓸 수 있지만, 최종 상태를 정하는 데 쓰지 마세요.
* 결제 웹훅을 받으면 `GET /v1/invoice-payments?month=YYYY-MM`나 `GET /v1/invoices/{id}/payment-attempts`로 지금 상태를 다시 읽고 그 값으로 갱신하세요. 결과 확인 방법은 [첫 결제 요청과 결과 확인](https://docs.meterengine.com/payments/charging.md)을 보세요.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.