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

# 사용량 이벤트 보내기

> 사용량 이벤트를 한 건씩 또는 묶어서 보내는 방법과 응답을 설명합니다.

이벤트는 고객이 서비스를 쓴 사실 하나입니다(API 호출 한 번, 토큰 사용량 한 번 등). 도입사 서버가 이벤트를 MeterEngine에 보내면, MeterEngine이 미터 정의에 따라 달마다 집계하고 단가를 곱해 청구 예정액을 만듭니다.

MeterEngine은 SDK를 배포하지 않으므로 예시는 HTTP 요청과 코드 조각으로 보여 줍니다. [소개](https://docs.meterengine.com/index.md)를 보세요.

## 보내기 전에

* 이벤트를 받을 미터와 이번 달 단가를 먼저 만드세요. 준비 순서는 [동작 방식](https://docs.meterengine.com/concepts/how-it-works.md)에 있습니다.
* 이벤트의 `customer_id`에는 고객 등록 응답의 `id`(UUID)를 넣습니다. [고객 등록과 ID 대응](https://docs.meterengine.com/integration/customers.md)을 보세요.
* ingest 권한이 있는 API 키가 필요합니다. 키 발급 화면의 기본 권한은 read뿐이라 ingest도 골라야 합니다. [API 키](https://docs.meterengine.com/authentication/api-keys.md)를 보세요.

## 이벤트 필드

| 필드 | 필수 | 형식과 뜻 |
| - | - | - |
| `transaction_id` | 예 | 1자에서 255자 문자열. 이벤트마다 고유한 값입니다. 같은 값으로 다시 보내면 저장하지 않고 처음 것을 유지합니다 |
| `customer_id` | 예 | MeterEngine 고객 id(UUID). 같은 업무 환경에 등록한 고객이어야 합니다 |
| `type` | 예 | 공백이 아닌 문자열. 미터의 `event_type`과 같은 값이어야 그 미터에 집계됩니다 |
| `properties` | 예 | JSON 객체. 비어 있어도(`{}`) 됩니다. 미터의 집계 속성과 차원 속성을 여기에 담습니다 |
| `timestamp` | 예 | 사용이 일어난 시각. 오프셋을 붙인 ISO 8601(`2026-11-05T10:00:00+09:00`)로 보냅니다 |

* 서버가 받은 시각(`received_at`)은 MeterEngine이 정합니다. 요청에 넣어도 무시합니다.
* `transaction_id`에는 도입사 시스템에서 그 사용을 가리키는 값을 씁니다(요청 id 등). 보낼 때마다 새로 만드는 값을 쓰면 다시 보낸 이벤트가 중복으로 저장됩니다. 멱등 규칙은 [멱등 규칙](https://docs.meterengine.com/api-basics/idempotency.md)에 모았습니다.
* 집계 속성 값은 JSON 숫자로 보냅니다(`12`). 문자열 `"12"`는 합계, 최솟값 같은 숫자 집계에 들어가지 않습니다. 미터 설계는 [미터 설계](https://docs.meterengine.com/pricing/meters.md), 차원 속성은 [차원](https://docs.meterengine.com/pricing/dimensions.md)을 보세요.
* `properties`의 숫자는 자릿수를 자르지 않고 저장합니다. 키 순서와 공백은 보존하지 않습니다.
* 이벤트가 드는 달은 `timestamp`의 한국 시간(KST) 기준 달력 월입니다. [시간 규칙](https://docs.meterengine.com/concepts/time-rules.md)을 보세요.

## 한 건 보내기

> [!WARNING]
>
> 보낸 이벤트는 고치거나 지울 수 없습니다. 테스트 환경도 같습니다. 처음에는 시험 전용 고객과 미터로 보내고, 잘못 보냈을 때는 이 페이지 아래의 '잘못 보낸 이벤트 바로잡기'를 보세요.

```bash curl theme={null}
curl -X POST https://meterengine.com/v1/events \
  -H "Authorization: Bearer me_test_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "req_20261105_0001",
    "customer_id": "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
    "type": "api_call",
    "timestamp": "2026-11-05T10:00:00+09:00",
    "properties": { "units": 12, "region": "seoul" }
  }'
```

```javascript Node theme={null}
const response = await fetch("https://meterengine.com/v1/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.METERENGINE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    transaction_id: "req_20261105_0001",
    customer_id: "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
    type: "api_call",
    timestamp: "2026-11-05T10:00:00+09:00",
    properties: { units: 12, region: "seoul" },
  }),
});
const result = await response.json();
// 200이면 저장됐거나(duplicate: false) 이미 저장돼 있던 것(duplicate: true)입니다.
```

```json 200 OK theme={null}
{
  "transaction_id": "req_20261105_0001",
  "duplicate": false
}
```

**transaction\_id** `string`

요청에 보낸 `transaction_id`.

**duplicate** `boolean`

`false`면 이번 요청으로 저장했습니다. `true`면 같은 `transaction_id`의 이벤트가 이미 있어 저장하지 않았고 처음 것을 유지합니다. 둘 다 성공으로 처리하세요.

### 단건 응답

| 상태 | code | 뜻 |
| - | - | - |
| 200 | | 저장했거나(`duplicate: false`) 이미 저장돼 있습니다(`duplicate: true`) |
| 400 | `malformed_request_body` | 본문을 JSON으로 읽을 수 없거나 값의 타입이 맞지 않습니다. `timestamp`에 오프셋이 없거나 `customer_id`가 UUID 형식이 아니어도 이 code입니다 |
| 400 | `validation_error` | 필수 필드가 없거나 형식이 틀렸습니다. `errors`에 틀린 필드가 있습니다 |
| 400 | `event_too_old` | `timestamp`가 MeterEngine이 받은 시각보다 35일 넘게 과거입니다. 정확히 35일 전은 받습니다 |
| 404 | `customer_not_found` | `customer_id`의 고객이 이 업무 환경에 없습니다 |
| 409 | `customer_archived` | `customer_id`의 고객을 보관했습니다 |

```json 400 event_too_old theme={null}
{
  "code": "event_too_old",
  "message": "받는 기간이 지난 이벤트입니다",
  "errors": [
    { "field": "timestamp", "message": "수집 판정 시각보다 35일 넘게 과거인 시각입니다" }
  ]
}
```

이미 저장된 `transaction_id`를 다시 보내면, 그 사이 고객을 보관했거나 35일이 지났어도 거절하지 않고 `200`과 `duplicate: true`로 답합니다. `customer_id`가 이 업무 환경에 없는 고객이면 저장 여부와 관계없이 `404 customer_not_found`입니다. 오류 형식과 code로 분기하는 규칙은 [오류 응답](https://docs.meterengine.com/api-basics/errors.md)에 있고, 어떤 오류를 다시 보낼지는 [재전송 설계](https://docs.meterengine.com/integration/retries.md)에 있습니다.

## 묶어서 보내기

`POST /v1/event-batches`는 한 요청에 1건에서 100건까지 받습니다. 원소마다 따로 판정하므로 한 원소가 틀려도 나머지는 저장됩니다. 결과는 보낸 순서대로 `results`에 실립니다.

```bash curl theme={null}
curl -X POST https://meterengine.com/v1/event-batches \
  -H "Authorization: Bearer me_test_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "transaction_id": "req_20261105_0002",
        "customer_id": "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
        "type": "api_call",
        "timestamp": "2026-11-05T10:01:00+09:00",
        "properties": { "units": 8, "region": "seoul" }
      },
      {
        "transaction_id": "req_20261105_0001",
        "customer_id": "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
        "type": "api_call",
        "timestamp": "2026-11-05T10:00:00+09:00",
        "properties": { "units": 12, "region": "seoul" }
      },
      {
        "transaction_id": "req_20261105_0003",
        "customer_id": "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
        "type": "api_call",
        "timestamp": "2026-11-05T10:02:00",
        "properties": { "units": 5, "region": "seoul" }
      },
      {
        "transaction_id": "req_20261105_0004",
        "customer_id": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
        "type": "api_call",
        "timestamp": "2026-11-05T10:03:00+09:00",
        "properties": { "units": 3, "region": "seoul" }
      }
    ]
  }'
```

```javascript Node theme={null}
const response = await fetch("https://meterengine.com/v1/event-batches", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.METERENGINE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ events }), // events: 1건에서 100건
});
const { results } = await response.json();
const rejected = results.filter(item => item.result === "rejected");
```

```json 200 OK theme={null}
{
  "results": [
    { "index": 0, "transaction_id": "req_20261105_0002", "result": "stored" },
    { "index": 1, "transaction_id": "req_20261105_0001", "result": "duplicate" },
    {
      "index": 2,
      "transaction_id": "req_20261105_0003",
      "result": "rejected",
      "error": {
        "code": "validation_error",
        "message": "요청 값이 올바르지 않습니다",
        "errors": [
          { "field": "events[2].timestamp", "message": "이벤트 필드의 형식이 올바르지 않습니다" }
        ]
      }
    },
    {
      "index": 3,
      "transaction_id": "req_20261105_0004",
      "result": "rejected",
      "error": { "code": "customer_not_found", "message": "고객을 찾을 수 없습니다" }
    }
  ]
}
```

**results** `array`

보낸 순서대로 원소마다 결과 하나.

**원소 결과**

**index** `integer`

요청 `events` 배열에서의 위치(0부터).

**transaction\_id** `string | null`

그 원소의 `transaction_id`. 원소에 문자열 `transaction_id`가 없으면 `null`입니다.

**result** `string`

`stored`(저장함), `duplicate`(이미 저장돼 있음), `rejected`(거절함) 중 하나.

**error** `object`

`rejected`일 때만 실립니다. 단건 오류 응답과 같은 `{code, message, errors}` 형태이고, `errors`의 `field`는 `events[2].timestamp`처럼 원소 위치를 붙여 씁니다. code는 `validation_error`, `event_too_old`, `customer_not_found`, `customer_archived` 중 하나입니다.

> [!WARNING]
>
> 원소 중 일부가 거절돼도 HTTP 상태는 `200`입니다. 상태 코드만 보고 성공으로 처리하지 말고 `results`의 원소를 하나씩 확인하세요. `stored`와 `duplicate`는 성공, `rejected`는 `error.code`로 판단합니다.

단건과 다른 점:

* `timestamp`에 오프셋이 없거나 `customer_id`가 UUID 형식이 아니어서 읽을 수 없는 원소는 그 원소만 `validation_error`로 거절합니다(단건은 요청 전체가 `400 malformed_request_body`). 이때 `errors`의 `field`는 `events[2].timestamp` 또는 `events[2]`처럼 원소 위치로 나옵니다.
* 같은 요청 안에 같은 `transaction_id`가 두 번 있으면 먼저 받아들인 것만 저장하고 뒤의 것은 `duplicate`입니다.
* `events`가 없거나 비었거나 100건을 넘으면 원소를 보지 않고 요청 전체를 `400 validation_error`로 거절합니다. 본문을 JSON으로 읽을 수 없으면 `400 malformed_request_body`입니다.
* 분당 요청 한도를 정한 키에서는 묶음 하나가 요청 한 번입니다. [요청 한도](https://docs.meterengine.com/api-basics/rate-limits.md)를 보세요.

## 청구에 들어가는 이벤트

`200`을 받았다는 것은 저장했다는 뜻이고, 청구된다는 뜻은 아닙니다. 저장한 이벤트가 청구 예정액에 들어가려면 다음이 모두 맞아야 합니다.

1. `type`과 같은 `event_type`을 가진 미터가 있습니다. 미터가 없는 `type`도 거절하지 않고 저장합니다.
2. 숫자 집계 미터라면 집계 속성 값이 JSON 숫자입니다.
3. 차원 속성 값의 조합에 맞는 단가가 그 달에 있습니다. 단가가 없는 조합의 사용량은 청구되지 않습니다.
4. 지난달 이벤트는 다음 달 1일 13:00 KST까지 MeterEngine이 받은 것만 청구됩니다. 그 뒤에 받은 지난달 이벤트도 `200`으로 저장되지만 청구되지 않습니다. [시간 규칙](https://docs.meterengine.com/concepts/time-rules.md)을 보세요.

보낸 이벤트가 저장됐는지는 read 권한 키로 `GET /v1/events/lookup?transaction_id=req_20261105_0001`을 불러 확인합니다. 없으면 `404 event_not_found`입니다. 저장한 뒤 집계와 금액을 확인하는 순서는 [보낸 데이터 확인하기](https://docs.meterengine.com/integration/verify-usage.md)에, 저장됐는데 청구되지 않을 때는 [이벤트가 청구되지 않을 때](https://docs.meterengine.com/troubleshooting/events-not-billed.md)에 있습니다.

## 잘못 보낸 이벤트 바로잡기

보낸 이벤트는 고치거나 지울 수 없습니다. 같은 `transaction_id`로 다시 보내도 처음 것이 남습니다.

집계가 `sum`인 미터는 확정 전에 상계 이벤트로 바로잡을 수 있습니다.

**1. 상계 이벤트를 만든다**

잘못 보낸 이벤트와 같은 `customer_id`, `type`, 차원 속성 값을 쓰고, 집계 속성에 빼야 할 만큼의 음수 값을 넣습니다. `transaction_id`는 새 값을 씁니다.

**2. 같은 달 시각을 쓴다**

`timestamp`는 원래 이벤트와 같은 달(KST)이어야 그 달 금액에 반영됩니다. 지난달 이벤트를 바로잡는다면 다음 달 1일 13:00 KST 전에 보내야 합니다.

**3. 청구 예정액으로 확인한다**

`GET /v1/invoices/draft?month=YYYY-MM` 또는 콘솔 '청구 검토' 화면에서 금액이 바뀌었는지 봅니다.

예를 들어 `units` 12를 보내야 했는데 120을 보냈다면, 같은 고객과 `type`, 같은 `region` 값으로 `units` -108을 새 `transaction_id`로 보냅니다.

```json theme={null}
{
  "transaction_id": "req_20261105_0001_fix",
  "customer_id": "3f6c2a8e-5b1d-4c7a-9e2f-8d4b6a1c0e57",
  "type": "api_call",
  "timestamp": "2026-11-05T10:00:00+09:00",
  "properties": { "units": -108, "region": "seoul" }
}
```

> [!WARNING]
>
> 한 조합(고객, 미터, 차원 값)의 합이 음수가 되면 그 조합은 0원으로 보고 인보이스 라인에서 빠집니다. 이런 조합은 청구 예정액의 `negative_usages`에 나타나고, 웹훅으로 `unbilled_usage_detected`를 받도록 등록했다면 그 통보에도 실립니다([웹훅 이벤트 유형과 data](https://docs.meterengine.com/webhooks/events.md)). 너무 많이 빼지 않았는지 확인하세요.

> [!WARNING]
>
> `count` 미터는 값과 관계없이 이벤트 수를 세고 `count_distinct` 미터는 서로 다른 값의 수를 세므로, 상계 이벤트를 보내면 결과가 줄지 않고 오히려 늘 수 있습니다. `min`, `max`, `avg`, `first`, `last`, `median`, `percentile` 미터도 음수 값이 결과를 바꿀 뿐 되돌리지 않습니다. 이 미터들은 보내기 전에 검증하는 것 말고 정정 방법이 없습니다. 확정한 달은 어느 미터든 정정할 수 없습니다. [확정한 뒤](https://docs.meterengine.com/billing/after-finalization.md)를 보세요.


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