엔드포인트
`webhooks.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete`, `rotate_secret`, `test`, `get_delivery`, `replay_delivery`, 그리고 전송 로그와 활동 로그.
모든 메서드
endpoint = client.webhooks.create( url: "https://acme.com/hooks/mail", eventTypes: ["email.sent", "email.bounced"], description: "Billing service") File.write(".openemail-webhook-secret", endpoint[:secret]) client.webhooks.listclient.webhooks.get(endpoint[:id])client.webhooks.update(endpoint[:id], enabled: false)client.webhooks.test(endpoint[:id])latest = client.webhooks.list_deliveries(endpoint[:id], limit: 1).items.firstclient.webhooks.get_delivery(endpoint[:id], latest[:id])client.webhooks.replay_delivery(endpoint[:id], latest[:id])rotated = client.webhooks.rotate_secret(endpoint[:id])File.write(".openemail-webhook-secret", rotated[:secret])client.webhooks.delete(endpoint[:id])시크릿이 반환되는 것은 rotate_secret을 빼면 create 때뿐입니다. 읽기 호출은 절대 시크릿을 되돌려주지 않으므로, 다른 일을 하기 전에 먼저 저장하세요. eventTypes를 생략하면 기본 집합, 즉 email.replied를 제외한 모든 email.* 이벤트를 받습니다. email.replied, domain.*, suppression.*, file.*, form.*는 엔드포인트가 이를 명시할 때만 전달됩니다.
rotate_secret에는 겹침 구간이 없습니다. 예전 시크릿은 즉시 동작을 멈추므로, 회전하기 전에 새 시크릿을 먼저 배포하세요. 자동으로 재시도되지 않습니다: 재시도하면 두 번째 회전이 일어나 첫 번째 시도가 돌려준 시크릿이 무효가 되기 때문입니다.
create도 재시도되지 않으므로, 네트워크 장애로 본 적 없는 시크릿을 가진 엔드포인트가 만들어진 채 남을 수 있습니다. 다시 만들기 전에 list를 확인하세요. 워크스페이스는 기본적으로 엔드포인트를 10개까지 가질 수 있으며, 한도를 넘는 다음 엔드포인트는 422 workspace_limit_reached입니다.
구독할 수 있는 이벤트
OpenEmail::WEBHOOK_EVENTS는 모든 이벤트 이름을 담은 동결된 Hash이므로 요청 없이 목록을 렌더링할 수 있으며, webhooks.list_events는 같은 이름을 각각의 설명 문장, 그리고 엔드포인트에 적용되는 한도와 함께 반환합니다. 이 이벤트는 이 API의 이벤트가 아니라 **메일함**의 이벤트입니다: email.received는 앱에 메일이 도착할 때 발생하고, email.sent는 작성기가 메시지를 보냈을 때 발생합니다. 구독하는 것은 자신의 API 트래픽을 지켜보는 것과 다릅니다.
file.uploaded는 파일이 파일 페이지에 올라갈 때, file.deleted는 파일이 삭제될 때 발생합니다. 그 data는 fileId, filename, mimeType, sizeBytes, direction, to, threadId, messageId, 그리고 uploadedAt 또는 deletedAt을 담습니다. to는 파일이 속한 주소이고, 워크스페이스 전체에 속한 파일이면 nil입니다.
파일 이벤트는 기본 집합에 없으므로, 엔드포인트가 eventTypes에 명시할 때만 받습니다. 일부 주소로 제한된 엔드포인트는 그 주소의 파일에 관한 이벤트만 받으므로, to가 nil인 워크스페이스 전체용 업로드는 전달되지 않습니다.
form.submitted는 누군가 내 양식 중 하나로 가입할 때 발생하고, form.confirmed는 그 사람이 확인 링크를 열었거나 내가 승인해서 확인 대기 중인 가입이 오디언스에 들어갈 때 발생합니다. form.submitted의 data에는 formId, formName, submissionId, email, status, answers, audienceIds, sourceUrl, submittedAt이 들어 있습니다. form.confirmed의 data에는 formId, formName, submissionId, email, audienceIds, link 또는 approval인 via, 그리고 confirmedAt이 들어 있습니다.
더블 옵트인이 없는 양식에서의 가입은 status가 added인 form.submitted를 보내고 form.confirmed는 보내지 않으므로, 그 조합을 누군가 들어온 순간으로 취급하십시오. 확인 전에 다시 가입한 사람은 같은 submissionId를 유지하며, form.submitted는 응답이 바뀐 경우에만 다시 전송됩니다. 양식 이벤트는 기본 집합에 없으며, 가입은 워크스페이스 전체에 속하므로 일부 주소로 제한된 엔드포인트는 이 이벤트를 받지 않습니다.
동작 확인하기
result = client.webhooks.test("whe_3f9c2a7b1e4d8f60a5c7b92d")puts result.dig(:delivery, :status), result.dig(:delivery, :responseCode) client.webhooks.iterate_deliveries("whe_3f9c2a7b1e4d8f60a5c7b92d") do |delivery| puts "#{delivery[:eventType]} #{delivery[:status]} #{delivery[:responseCode]} #{delivery[:error]}"endtest는 서명된 합성 email.sent 이벤트를 POST하고 시도가 끝날 때까지 기다립니다. 수신 측이 무엇을 응답하든 정상적으로 반환되므로, 호출이 예외를 발생시켰는지가 아니라 delivery[:status]로 분기하세요. 4xx는 유용한 답입니다: URL에 닿을 수 있고, 거부는 여러분 자신의 핸들러, 흔히 그 서명 검사에서 왔다는 뜻이기 때문입니다.
responseCode가 nil이면 응답이 아예 없었다는 뜻이며(DNS, TLS, 타임아웃), 이는 응답이 0이라고 말한 것과는 다른 사실입니다. 각 행에는 attempt와 maxAttempts가 담기므로 여러 행이 하나의 이벤트를 설명할 수 있습니다: 행들에 걸쳐 동일한 eventId가 그 이벤트이고, 시도 번호가 각각의 시도입니다. nextAttemptAt은 그 행 다음의 자동 재시도가 언제 예정되어 있는지 알려 줍니다.
다시 보내기
detail = client.webhooks.get_delivery("whe_3f9c2a7b1e4d8f60a5c7b92d", "whd_8c1e4a7f2b9d3e6a0c5f1b28")p detail[:payload], detail[:responseBody], detail[:replayRefusal] replay = client.webhooks.replay_delivery("whe_3f9c2a7b1e4d8f60a5c7b92d", "whd_8c1e4a7f2b9d3e6a0c5f1b28")p replay.dig(:delivery, :status), replay.dig(:delivery, :responseCode)계속 실패하는 전달은 최대 8번 시도합니다. 즉시, 그리고 1분, 5분, 30분, 2시간, 5시간, 10시간, 다시 10시간 뒤이며, 모두 합쳐 약 27시간 반입니다. 반복할 가치가 있는 실패만 반복합니다. 응답 없음, 408, 425, 429, 5xx가 여기에 해당합니다. 재전송은 저장된 이벤트를 같은 id, type, createdAt, data로 다시 보내므로, 이미 처리한 id를 버리는 수신 측은 이를 이미 아는 이벤트로 취급합니다. 새로 바뀌는 것은 서명뿐입니다.
replay_delivery는 이벤트 하나를 지금 보내고 서버가 응답한 내용을 반환합니다. 전달에 성공한 시도에도 쓸 수 있고, 절대 재시도되지 않습니다. 보내기 전에 그 이벤트의 자동 재시도 가운데 아직 시작되지 않은 것은 일시 중지됩니다: 재전송이 전달되면 계속 취소된 상태로 남고, 실패하면 예정대로 다시 진행됩니다.- 그 순간 같은 이벤트의 자동 재시도가 전송 중이면
replay_delivery는 아무것도 보내지 않고 409retry_in_progress를 발생시키고, 같은 이벤트의 다른 재전송이 아직 전송 중이면 409replay_in_progress를 발생시키므로, 같은 순간에 보낸 두 재전송이라도 수신 측이 동시에 두 개를 받는 일은 없습니다. 몇 초 기다린 뒤get_delivery를 확인하세요. 그 재시도나 재전송으로 전달될 수 있습니다. 재전송은 한 번에 이벤트 하나입니다: 실패한 전달을 모두 다시 보내는 호출은 없습니다. - 또한 꺼진 엔드포인트(
webhook_disabled), 엔드포인트가 더 이상 구독하지 않는 이벤트(event_not_subscribed)나 더 이상 포함하지 않는 이벤트(event_out_of_scope), 저장된 이벤트가 없는 시도(delivery_not_replayable)에도 409를 발생시킵니다.get_delivery는 그 결과를replayRefusal로 미리 알려 줍니다.
gem은 replay_delivery를 스스로 재시도하지 않습니다. 응답을 잃은 뒤 재시도하면 이벤트가 한 번 더 전송되기 때문입니다.
파라미터: webhooks.create
urlString필수- 전달 요청이 POST될 주소입니다. HTTPS만 허용되며, 호스트는 `localhost`, `.localhost`·`.local`·`.internal` 이름, 루프백·사설·CGNAT·링크 로컬 IP 리터럴일 수 없습니다. 여러분이 지정한 주소로 서버가 직접 요청을 보내는 것이므로 그런 값은 `url`에 대한 422 `invalid_webhook_url`입니다. 이 검사는 적힌 그대로의 호스트 이름을 읽으며, 전달할 때마다 호스트를 다시 조회해 그 범위에 속한 주소로는 보내기를 거부합니다. 전달은 리디렉션을 따라가지 않으므로 최종 주소를 등록하세요. 저장되는 값은 보낸 것을 URL 파서가 다시 직렬화한 형태이므로, `https://acme.com`은 `https://acme.com/`으로 읽힙니다.
eventTypesArray<String>- 이 엔드포인트로 전달할 이벤트입니다: `OpenEmail::WEBHOOK_EVENTS`에 있는 값 중 아무거나 지정할 수 있습니다. `create`는 Array 길이를 존재하는 이벤트 수만큼으로 제한하므로 그보다 하나라도 많으면 `eventTypes`에 대한 422이며, `update`는 제한하지 않습니다. 제한되는 것은 길이뿐이고, 반복된 이름은 보낸 그대로 저장되어 그대로 읽힙니다. 생략하거나 비워 두면 빈 목록으로 저장되며, 그래서 읽을 때 `["*"]`로 나타납니다. 이는 `email.replied`를 제외한 모든 `email.*` 이벤트, 오늘 기준 열네 개를 뜻하며 도메인, 차단, 파일, 양식 계열은 결코 포함하지 않습니다. 나중에 추가되는 계열은 그 이름을 지정하지 않은 엔드포인트에 절대 도달하지 않으므로, 릴리스 때문에 통합이 한 번도 본 적 없는 형태를 받기 시작하는 일은 없습니다.
descriptionString- 엔드포인트에 붙이는 라벨이며 최대 200자입니다. 덕분에 웹훅 목록이 URL만 늘어선 열이 아니라 이름으로 읽힙니다. 생략하면 nil로 저장되고 nil로 반환됩니다.
addressAllowlistArray<String>- 이 엔드포인트가 소식을 받는 개별 주소입니다. 이벤트는 그것이 관련된 주소가 이 목록에 있거나, 그 도메인이 `domainAllowlist`에 있을 때 전달됩니다. 둘 다 비워 두면 엔드포인트는 워크스페이스가 소유한 모든 주소에 관한 소식을 받습니다. 최대 50개이며, 이 워크스페이스가 소유하지 않은 주소는 422 `invalid_parameter`입니다.
domainAllowlistArray<String>- 이 엔드포인트가 소식을 받는 도메인 전체로, 나중에 추가되는 주소도 포함합니다. 도메인은 자체 `domain.*` 이벤트도 전달합니다. 최대 25개입니다.
api_keyString- 클라이언트의 키 대신 이 키로 엔드포인트를 만듭니다.
응답: 만들어진 엔드포인트
Symbol 키를 가진 Hash입니다. get, list, update는 secret이 없는 같은 형태를 반환합니다.
objectString- 항상 `webhook`이며, 일반 읽기가 반환하는 것과 같은 판별자입니다. 시크릿은 별도의 객체 타입이 아니라 평범한 형태에 키 하나가 더해진 것이기 때문입니다. `secret`이 포함되는지는 이 필드가 아니라 어떤 메서드를 호출했는지로 결정됩니다.
idString- 엔드포인트의 식별자로, `whe_` 뒤에 16진수 24자가 붙습니다. 다른 모든 웹훅 호출이 이 값을 받습니다: `get`, `update`, `delete`, `rotate_secret`, `test`, `list_deliveries`, `list_all_deliveries`, `iterate_deliveries`, `get_delivery`, `replay_delivery`.
urlString- HTTPS 검사와 차단 호스트 검사를 통과해 저장된 엔드포인트 주소입니다. 파싱된 URL을 다시 직렬화한 값이므로, 보낸 String이 아니라 이 값과 비교하세요.
descriptionString or nil- 지정한 라벨이며, 지정하지 않았으면 nil입니다. `update`로 `description: nil`을 보내면 지워집니다.
eventTypesArray<String>- 구독한 이벤트이며, 엔드포인트가 아무것도 지정하지 않았으면 `["*"]`입니다. `["*"]`는 비어 있는 저장 목록이 읽을 때 표현되는 방식일 뿐 되돌려 보낼 수는 없으며, 전체 카탈로그가 아니라 열네 개의 메시지 이벤트를 뜻합니다. `create`와 `update`는 리터럴 이벤트 이름만 받습니다.
enabledBoolean- 전달을 시도할지 여부입니다. 비활성화된 엔드포인트는 이벤트를 발송할 때 건너뛰지만 시크릿과 전달 이력은 그대로 유지합니다. `enabled`를 받는 것은 `update`뿐이므로, 여기서는 항상 true입니다.
disabledAtString or nil- 연속 100번 전달에 실패한 뒤 서버가 엔드포인트를 끈 시각입니다. 켜져 있는 동안과, 직접 끈 경우에는 nil입니다.
disabledReasonString or nil- 서버가 끈 이유입니다. `disabledAt`이 nil이면 항상 nil입니다.
consecutiveFailuresInteger- 연속으로 실패한 전달 수입니다. 이벤트가 하나라도 전달되면 0으로 돌아가며, `enabled: true`를 준 `update`도 마찬가지입니다.
addressAllowlistArray<String>- 이 엔드포인트가 소식을 받는 개별 주소.
domainAllowlistArray<String>- 이 엔드포인트가 소식을 받는 도메인 전체. 두 목록이 모두 비어 있으면 워크스페이스가 소유한 모든 주소를 뜻합니다.
lastDeliveryAtString or nil- 마지막 성공이 아니라 마지막 전달 시도의 ISO 8601 타임스탬프입니다. 실패한 POST 뒤에도 찍히므로, 이 값은 엔드포인트를 시도했다는 사실을 알려 주고 결과가 어땠는지는 `list_deliveries`가 알려 줍니다. 첫 시도 전까지는 nil이므로 `create`에서는 언제나 nil입니다.
createdAtString- 엔드포인트가 등록된 시각의 ISO 8601 타임스탬프입니다. `list`는 이 필드를 기준으로 최신 엔드포인트부터 반환합니다.
secretString- 각 전달의 `X-OpenEmail-Signature`에 서명하는 HMAC-SHA-256 키입니다: `whsec_` 뒤에 base64url 문자 43개가 붙으며, 접두사를 포함해 `OpenEmail.verify_webhook_signature`에 넘기는 값입니다. `create`와 `rotate_secret`만 반환하고 그 밖에는 어디서도 반환하지 않습니다. 읽기 호출은 절대 되돌려주지 않으므로 지금 저장하세요. 잃어버린 시크릿은 `rotate_secret`으로만 교체할 수 있으며, 그러면 예전 시크릿은 즉시 무효가 됩니다.
로그 거르기
failed = client.webhooks.list_workspace_deliveries(status: "failed", since: Time.now - 86_400)p failed.items.map { |delivery| [delivery[:endpointId], delivery[:eventType], delivery[:responseCode]] } history = client.webhooks.list_activity("whe_3f9c2a7b1e4d8f60a5c7b92d")p history.items.map { |change| [change[:type], change.dig(:actor, :label)] }list_deliveries는 엔드포인트 하나를, list_workspace_deliveries는 모든 엔드포인트나 endpoint_ids:로 지정한 것을 읽으며, 둘 다 콘솔 전송 탭의 필터인 status:, since:, until:을 받습니다. list_activity와 list_workspace_activity는 감사 로그를 읽습니다: 누가 무엇을 만들고, 바꾸고, 끄거나 켜고, 교체하고, 테스트하고, 재전송하고, 삭제했는지. 각각 옆에 list_all_과 iterate_ 버전이 있고, 워크스페이스 로그의 모든 행에는 endpointId가 붙습니다. webhooks.stats는 원하는 기간에 대해 분석 탭 뒤에 있는 숫자를 반환합니다.
since:와 until:은 Time, DateTime, 또는 String으로 된 ISO 8601 시각을 받으며, Ruby의 Date는 그날의 UTC 자정을 뜻합니다. until은 Ruby 키워드지만, 다른 키워드 인자처럼 동작합니다: list_deliveries(id, since: start, until: finish).