오류
하나의 형태, 두 개의 층위, 그리고 모든 응답에 담기는 요청 id.
오류 봉투
type은 분기해도 되는 고정된 집합이며 앞으로도 늘어나지 않습니다. code는 구체적이고 계속 추가되므로, 알아보지 못하는 코드는 그 type으로 취급하세요. requestId는 성공을 포함한 모든 응답에 담기며, 제보와 로그 한 줄을 이어 주는 값입니다.
{ "error": { "type": "validation_error", "code": "invalid_email_address", "message": "Not a valid email address: ada@", "param": "to.0", "docUrl": "https://openemail.uk/docs/api/errors#invalid_email_address", "requestId": "req_e103790543af4…" }}param은 점과 인덱스로 표기되므로, 해당 요소를 담고 있는 필드가 아니라 정확한 요소(to.0, attachments.2.filename)를 가리킵니다.
상태 코드
| 상태 | type | 주요 코드 |
|---|---|---|
| 400 | invalid_request_error | malformed_json, invalid_idempotency_key |
| 401 | authentication_error | missing_api_key, invalid_api_key, revoked_api_key, invalid_credential_type |
| 403 | permission_error | insufficient_scope, from_address_forbidden |
| 404 | not_found_error | resource_not_found |
| 409 | conflict_error | email_not_cancellable, translation_not_configured |
| 422 | validation_error | invalid_email_address, reserved_header, too_many_recipients, unknown_parameter, idempotency_key_reuse, label_not_directly_settable, unknown_language, translation_too_long |
| 429 | rate_limit_error | send_quota_exceeded, too_many_inboxes |
| 500 | api_error | internal_error |
| 503 | api_error | translation_failed |
404는 "존재하지 않는다"와 "다른 워크스페이스에 속한다"를 절대 구분하지 않습니다. 의도적입니다. 그 차이 자체가 정보이기 때문입니다.
429에는 Retry-After가 절대 담기지 않으므로 대기 시간은 직접 정하세요. send_quota_exceeded는 월간 발송 허용량이고 매월 1일에 초기화되므로, 백오프하기보다 사람에게 보여 주세요. too_many_inboxes는 일회용 받은편지함 생성 상한이며, 이미 갖고 있는 받은편지함을 연장하는 데에는 이 상한이 소모되지 않습니다.
500과 503은 type을 공유하지만 호출자에게는 다른 의미입니다. 503은 응답하지 않은 의존성(현재로서는 번역기)이며 요청을 그대로 재시도할 가치가 있습니다. 500은 우리 쪽 문제이며 requestId와 함께 제보할 가치가 있습니다.
오류 처리
동작은 type으로 분기하고, 사람에게 보여 줄 메시지는 code로 읽으세요. 알아보지 못하는 code는 여러분 클라이언트의 오류가 아닙니다. 우리가 실패를 예전보다 더 정확하게 이름 붙였다는 뜻입니다.
const res = await fetch(`${BASE}/emails`, { method: 'POST', headers, body }); if (!res.ok) { const { error } = await res.json(); switch (error.type) { case 'rate_limit_error': throw new RateLimited(error.code); case 'validation_error': // error.param points at the offending field throw new BadRequest(`${error.param}: ${error.message}`); case 'authentication_error': // revoked_api_key and expired_api_key are worth telling an operator apart throw new AuthFailed(error.code); default: // quote requestId when you report it throw new Unexpected(error.message, error.requestId); }}