빌링 API
콘솔을 열지 않고도 자신의 워크스페이스의 사용량과 비용 데이터를 자체 시스템으로 가져올 수 있습니다. 빌링 키는 읽기 전용 자격 증명이며 하나의 워크스페이스로 범위가 제한됩니다 - 청구 데이터를 읽을 수 있을 뿐, 그 외에는 아무것도 할 수 없습니다.
빌링 키는 이후에 삭제되거나 폐기된 키를 포함하여 해당 워크스페이스에 기록된 모든 요청을 읽을 수 있지만, 모델을 호출할 수 없으며 API 키를 생성, 수정, 삭제할 수도 없습니다. 이 외의 다른 곳에서 사용하면 401 wrong_key_kind.
빌링 키 만들기
- 열기: Console → Billing API keys (워크스페이스 소유자만 가능). 이름을 지정하고 필요하면 IP 허용 목록으로 제한하거나 만료일을 설정한 다음 키를 복사하세요 - 키는
sk-syn-bill-로 시작하며 한 번만 표시됩니다. - 이를 Bearer 토큰으로 사용해 아래 엔드포인트를 호출하세요. 같은 페이지에서 언제든지 폐기할 수 있습니다 - 폐기하면 즉시 무효화되며 워크스페이스의 다른 것에는 영향이 없습니다.
- 키는 생성자에게 연결되어 있습니다. 생성자가 더 이상 워크스페이스 소유자가 아니게 되면 작동을 멈추고
403 key_owner_not_authorized.
인증
모든 호출에는 Authorization: Bearer sk-syn-bill-... 를 아래 기본 URL에 대해 보내야 합니다. 빌링 키는 이 엔드포인트에서만 동작하며, 이 엔드포인트도 빌링 키만 허용합니다 - 그 외의 조합은 401 wrong_key_kind.
Authorization: Bearer sk-syn-bill-... Base URL: https://synthorai.io/api/v1/billing
범위는 워크스페이스 전체입니다. 삭제된 키를 포함하여 그 안의 모든 추론 키가 대상이며, 이력 행은 계속 조회할 수 있습니다. api_key_id 및 model 필터는 결과를 좁히기만 할 수 있으며 넓힐 수는 없습니다. 모든 조회는 항상 그 키 자신의 워크스페이스로 고정됩니다.
모든 조회는 읽기 전용 레플리카에서 실행되며 프라이머리에서는 실행되지 않습니다. 레플리카가 설정되지 않은 프로세스는 503 replica_unavailable 조용히 대체(fallback)되지 않습니다.
토큰 필드: OpenAI 규칙을 따릅니다 - prompt_tokens 은 총 입력 토큰 수이며 아래의 모든 캐시 읽기와 캐시 쓰기를 이미 포함합니다. 그리고 completion_tokens 은 이미 reasoning_tokens를 포함합니다. 둘 다 합계에 추가로 더해지는 것이 아니라 세부 내역일 뿐, 별도의 토큰이 아닙니다.
prompt_tokens 중 캐시 읽기 부분이 cached_tokens이고, 캐시 쓰기 부분이 cache_write_tokens입니다. /records 행에서는 TTL에 따라 cache_write_5m_tokens와 cache_write_1h_tokens로 더 세분화됩니다.
금액 필드: cost_usd 은 실제로 청구된 금액입니다. byok_list_price_usd 은 BYOK 요청이 정가로 부과되었을 경우의 비용입니다 (BYOK가 아니면 0). list_price_usd 이 API 어디에도 존재하지 않습니다.
여기서 "오류"는 API에 도달했지만 거부된 모든 요청을 의미합니다 - 402(잔액 부족), 403, 429 같은 4xx뿐 아니라 업스트림 실패도 포함됩니다. 제외되는 것은 우리 자체 내부 인프라 거부뿐이며, 콘솔과 동일한 기준입니다. 거부된 요청은 서버마다 key, 상태 코드, 분 단위로 최대 한 번 기록되므로 402, 403, 429 횟수는 하한값입니다. 성공한 요청과 과금은 완전합니다.
GET /usage - 집계 사용량
GET /usage
시간 또는 일 단위로 미리 집계된 행으로, 청구서와 대시보드용입니다. 시간별 롤업에 마지막 수집 이후 기록된 요청을 더하므로, 최근 버킷이 수집 주기 하나만큼 통째로 뒤처지지 않습니다.
| 매개변수 | 유형 | 설명 |
|---|---|---|
start* | string | 범위 시작: RFC 3339 타임스탬프 또는 2026-09-01과 같은 날짜. |
end | string | 범위 종료. 기본값은 현재 시각입니다. |
granularity | string | 버킷 크기: hour 또는 day. 기본값은 day입니다. |
group_by | string | 행을 나눌 차원: api_key, model, 또는 둘 다 (임의의 부분집합). 기본값은 둘 다입니다. |
api_key_id | integer | 하나 이상의 API 키 id로 필터링합니다. 반복 가능하며 범위를 좁히기만 하고 넓히지 않습니다. |
model | string | 하나 이상의 모델 id로 필터링합니다. 반복 가능합니다. |
limit | integer | 페이지당 행 수. 기본값 1000, 최대 5000. |
cursor | string | 이전 페이지의 meta.next_cursor에서 가져온, 불투명한 페이지네이션 커서. |
각 행에서 api_key_id와 api_key_name은 group_by api_key를 포함할 때만 나타나며, model도 마찬가지로 model을 포함할 때만 나타납니다. 버킷에 지연 시간 샘플이 없으면 avg_latency_ms가 null일 수 있습니다.
요청 예시
curl "https://synthorai.io/api/v1/billing/usage?start=2026-09-01&end=2026-09-24&granularity=day" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{
"data": [
{
"bucket_start": "2026-09-23T00:00:00Z",
"bucket_end": "2026-09-24T00:00:00Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"requests": 1280,
"success_requests": 1265,
"error_requests": 15,
"errors_4xx": 12,
"errors_429": 2,
"errors_5xx": 1,
"prompt_tokens": 512000,
"completion_tokens": 98000,
"cached_tokens": 210000,
"cache_write_tokens": 8000,
"reasoning_tokens": 4000,
"cost_usd": 4.1732,
"byok_list_price_usd": 0,
"byok_requests": 0,
"avg_latency_ms": 812,
"final": true
}
],
"meta": {
"generated_at": "2026-09-24T08:00:00Z",
"data_complete_until": "2026-09-24T06:00:00Z",
"timezone": "UTC",
"currency": "USD",
"next_cursor": null,
"has_more": false
}
} GET /records - 요청 단위 행
GET /records
요청마다 한 행이며, 자체 데이터베이스로의 증분 동기화를 위한 것입니다. cursor로 동기화하세요. 절대 시간으로 동기화하지 마세요. 수신한 행은 record_id로 중복 제거하세요 - 이유는 아래 "시간 공백 피하기"를 참고하세요.
| 매개변수 | 유형 | 설명 |
|---|---|---|
cursor | string | 이전 호출의 meta.next_cursor에서 가져온, 불투명한 동기화 커서(문자열). cursor와 start 중 정확히 하나만 지정하세요. 범위 제한이 없습니다. |
start | string | 범위 시작 (RFC 3339 타임스탬프 또는 날짜). cursor와 start 중 하나만 지정하세요. start/end 범위는 최대 31일이지만, cursor에는 범위 제한이 없습니다. |
end | string | 범위 종료. 기본값은 현재 시각입니다. |
api_key_id | integer | 하나 이상의 API 키 id로 필터링합니다. 반복 가능하며 범위를 좁히기만 하고 넓히지 않습니다. |
model | string | 하나 이상의 모델 id로 필터링합니다. 반복 가능합니다. |
limit | integer | 페이지당 행 수. 기본값 500, 최대 1000. |
요청 예시
curl "https://synthorai.io/api/v1/billing/records?start=2026-09-24T00:00:00Z&limit=500" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{
"data": [
{
"record_id": "rec_8f2a91c4",
"request_id": "req_9f3c2b1a",
"recorded_at": "2026-09-24T05:58:31Z",
"completed_at": "2026-09-24T05:58:29Z",
"api_key_id": 42,
"api_key_name": "prod-backend",
"model": "claude-sonnet-5",
"status": "success",
"status_code": 200,
"prompt_tokens": 812,
"completion_tokens": 194,
"cached_tokens": 512,
"cache_write_tokens": 64,
"cache_write_5m_tokens": 64,
"cache_write_1h_tokens": 0,
"cost_usd": 0.00412,
"byok_list_price_usd": 0,
"is_byok": false,
"is_stream": true,
"duration_ms": 1340,
"ttft_ms": 210
}
],
"meta": {
"next_cursor": "eyJvIjoiODgxNDA5MCJ9",
"has_more": false,
"window_final": true,
"safe_until": "2026-09-24T05:58:31Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"data_complete_until": "2026-09-24T05:58:00Z"
}
} | 매개변수 | 설명 |
|---|---|
next_cursor | 다음 호출을 위한 커서입니다. 항상 존재합니다. |
has_more | 지금 이 커서로 다시 호출하면 더 많은 행이 반환되는지 여부입니다. |
window_final | end를 지정했고 그것이 safe_until 이전이거나 같을 때만 true입니다 - 그때 요청한 범위가 완전함이 보장됩니다. 그 외에는 없거나 false입니다. |
safe_until | 호출자가 확정된 것으로 신뢰할 수 있는 가장 최근의 recorded_at입니다. 이보다 최신인 행은 아직 보류 중입니다. |
latest_recorded_at | 확정 여부와 관계없이 모든 행 중 가장 최근의 recorded_at입니다. |
data_complete_until | /freshness가 반환하는 값과 동일합니다 - 시간별 롤업이 어디까지 완료되었는지를 나타내며, /usage와 상호 대조하는 데 사용합니다. |
시간 기준: /usage는 completed_at 로 버킷화됩니다 (요청이 완료된 시각). /records의 start/end 필터는 recorded_at 로 필터됩니다 (행이 기록된 시각으로, 부하가 높을 때는 완료 시각보다 늦을 수 있습니다). /records를 /usage와 맞추려면 동기화한 행을 recorded_at이 아니라 completed_at 기준으로 직접 버킷화하세요.
GET /records/export - 스트리밍 내보내기
GET /records/export
/records와 동일한 필터를 사용하며, 줄바꿈으로 구분된 JSON(application/x-ndjson)으로 스트리밍됩니다 - 페이지가 나뉜 목록이 아니라 대규모 백필용으로 만들어졌습니다. 한 워크스페이스당 한 번에 하나의 내보내기만 스트리밍할 수 있으며, 두 번째는 429 too_many_concurrent_requests를 받습니다.
요청 예시
curl -N "https://synthorai.io/api/v1/billing/records/export?cursor=eyJvIjoiODgxNDAzMiJ9" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{"record_id":"rec_8f2a91c4","request_id":"req_9f3c2b1a","model":"claude-sonnet-5","status":"success","cost_usd":0.00412}
{"record_id":"rec_8f2a91c5","request_id":"req_9f3c2b1b","model":"claude-sonnet-5","status":"success","cost_usd":0.00398}
{"_meta":{"next_cursor":"eyJvIjoiODgxNDA5MSJ9","rows":2,"complete":true,"window_final":true,"generated_at":"2026-09-24T06:00:02Z"}} 스트림의 마지막 줄은 _meta 객체입니다:
| 매개변수 | 설명 |
|---|---|
next_cursor | 다음 내보내기 호출을 위한 커서입니다. |
rows | 이 스트림에서 기록된 레코드 수. |
complete | false는 범위를 모두 처리하기 전에 스트림이 멈췄다는 뜻입니다 - cursor=next_cursor로 재개하세요. |
window_final | /records와 동일한 의미입니다: 내보내기의 end가 safe_until 이전이거나 같을 때만 true입니다. |
generated_at | 이 줄이 기록된 시각입니다. |
stopped_because | complete가 false일 때 표시됩니다: row_cap, scan_cap(한 번에 훑을 수 있는 최대 id 범위를 다 훑었지만 결과가 차지 않음. 필터가 좁을 때 흔함), time_cap, query_error, window_not_final. |
GET /summary - 예비 통계 및 이상
GET /summary
총계, 비용 기준 상위 모델 및 상위 키, 해당 범위의 오류율, 그리고 anomalies 목록(high_error_rate, rate_limited, data_not_final, rollup_delayed)이 추가됩니다. 각각 info 또는 warning의 심각도와 사람이 읽을 수 있는 메시지를 포함합니다. 아직 확정되지 않은 범위의 숫자는 여전히 바뀔 수 있습니다 - 아래를 참고하세요.
| 매개변수 | 유형 | 설명 |
|---|---|---|
start | string | 범위 시작. 기본값은 end 이전 30일입니다. |
end | string | 범위 종료. 기본값은 현재 시각입니다. |
api_key_id | integer | 하나 이상의 API 키 id로 필터링합니다. 반복 가능하며 범위를 좁히기만 하고 넓히지 않습니다. |
model | string | 하나 이상의 모델 id로 필터링합니다. 반복 가능합니다. |
models_count와 keys_count는 top_models와 top_keys의 배후 총계를 나타내며, 이 목록들은 비용 기준 상위 최대 20개까지만 나열합니다. end 이전 30일이, start 가 생략된 경우의 기본 범위입니다.
요청 예시
curl "https://synthorai.io/api/v1/billing/summary?start=2026-09-17&end=2026-09-24" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{
"data": {
"totals": { "requests": 48210, "cost_usd": 132.55, "error_rate": 0.021 },
"models_count": 6,
"keys_count": 3,
"top_models": [ { "model": "claude-sonnet-5", "cost_usd": 88.10, "byok_list_price_usd": 0 } ],
"top_keys": [ { "api_key_id": 42, "api_key_name": "prod-backend", "cost_usd": 61.30, "byok_list_price_usd": 0 } ],
"anomalies": [
{ "type": "data_not_final", "severity": "info", "message": "The last 2 hours of this range are not yet final." }
]
},
"meta": {
"generated_at": "2026-09-24T06:00:02Z",
"data_complete_until": "2026-09-24T04:00:00Z"
}
} GET /freshness - 데이터 완전성
GET /freshness
반환되는 값은 data_complete_until, latest_recorded_at, safe_until 및 pipeline_lag_seconds - 방금 가져온 범위를 확정된 것으로 간주하기 전에 이를 폴링하세요. safe_until은 호출자가 완전하다고 신뢰할 수 있는 가장 최근의 recorded_at입니다. 그보다 최신인 행은 아직 도착 중일 수 있습니다.
요청 예시
curl "https://synthorai.io/api/v1/billing/freshness" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{
"data": {
"data_complete_until": "2026-09-24T04:00:00Z",
"latest_recorded_at": "2026-09-24T05:58:31Z",
"safe_until": "2026-09-24T05:58:31Z",
"pipeline_lag_seconds": 47
},
"meta": { "generated_at": "2026-09-24T06:00:02Z" }
} GET /balance - 워크스페이스에서 지금 쓸 수 있는 잔액
GET /balance
워크스페이스의 현재 사용 가능 잔액을 반환하며, 콘솔에 표시되는 사용 가능 잔액과 같습니다. 폴링용으로 설계되어 30~60초마다 확인하면 충분합니다. 별도의 속도 제한을 사용하므로 폴링이 데이터 엔드포인트의 한도를 소모하지 않으며, 잔액이 0일 때도 응답합니다.
요청 예시
curl "https://synthorai.io/api/v1/billing/balance" \
-H "Authorization: Bearer sk-syn-bill-..." 응답 예시
{
"data": {
"available_usd": 1284.37,
"debt_usd": 0,
"source": "live",
"voucher": null,
"scheduled_credits": [
{ "amount_usd": 250, "release_at": "2026-10-01T00:00:00Z" }
],
"scheduled_credit_total_usd": 250
},
"meta": { "generated_at": "2026-09-24T06:00:02Z", "currency": "USD" }
} | 매개변수 | 설명 |
|---|---|
available_usd | 현재 요청 과금에 쓰이는 일반 잔액. 0 아래로 내려가지 않습니다. 처리 중인 요청이 정산되면서 잠시 늘어날 수 있으며, 사용액은 /usage 또는 /records를 사용하세요. |
debt_usd | 미납 잔액: 충전액을 초과한 사용분입니다. 미납이 없으면 0입니다. 충전 시 미납 잔액이 먼저 차감되고, 남은 금액만 available_usd가 됩니다. |
source | live: 실시간 원장에서 읽은 값. delayed: 원장에 접근할 수 없어 최대 약 30초 늦을 수 있는 데이터베이스 사본에서 가져온 값. |
voucher | 프로모션/바우처 크레딧으로 이미지 생성 전용(applies_to)이며, 나열된 모델 패턴에서 expires_at까지 사용할 수 있습니다. 다른 요청은 available_usd만 사용하며 이 금액은 available_usd에 포함되지 않습니다. 없으면 null, 만료 후에는 active가 false입니다. |
scheduled_credits | 이미 합의되어 분할 지급되는 크레딧(amount_usd, release_at). 각 회차는 release_at 후 약 10분 이내에 잔액에 더해지며 그 전에는 사용할 수 없습니다. scheduled_credit_total_usd는 그 합계입니다. |
오류
모든 오류는 {"error": {"type", "message", "hint"}}:
{
"error": {
"type": "range_too_large",
"message": "the requested range exceeds this endpoint's cap",
"hint": "narrow start/end to at most 31 days, or sync /records with a cursor instead"
}
} | 유형 | HTTP 상태 | 의미 |
|---|---|---|
invalid_parameter | 400 | 쿼리 매개변수가 누락되었거나, 형식이 잘못되었거나, 범위를 벗어났습니다. |
range_too_large | 400 | 요청한 시간 범위 또는 id 윈도우가 해당 엔드포인트가 허용하는 범위보다 큽니다. hint에 해당 한도의 이름이 표시됩니다. |
start_too_recent | 400 | start/end 기간의 시작이 safe_until보다 늦어 시작 경계가 아직 확정되지 않았습니다. Retry-After 헤더의 시간 후 다시 시도하거나 cursor로 동기화하세요. |
wrong_key_kind | 401 | 이 자격 증명은 빌링 키가 아니거나, 빌링 키가 이 엔드포인트 외부에서 사용되었습니다. |
authentication_error | 401 | 키가 누락되었거나, 무효하거나, 만료되었거나, IP 허용 목록에 의해 차단되었습니다. |
key_owner_not_authorized | 403 | 이 키의 생성자는 더 이상 워크스페이스 소유자가 아닙니다. 빌링 키는 소유자 본인만 사용할 수 있으며, 소유권이 넘어가는 순간 작동을 멈춥니다. 새 소유자는 자신의 키를 만들어야 합니다. |
rate_limited | 429 | 이 워크스페이스에서 분당 60회를 초과한 요청이 발생했습니다. Retry-After 헤더에 표시된 시간 이후에 재시도하세요. |
too_many_concurrent_requests | 429 | 이 워크스페이스의 동시 요청이 3개를 넘었거나, 다른 내보내기가 이미 진행 중입니다. |
replica_unavailable | 503 | 이 프로세스에는 읽기 전용 레플리카가 설정되어 있지 않습니다. API는 프라이머리로 대체되지 않습니다. |
query_timeout | 503 | 쿼리가 10초 문장 타임아웃 또는 15초 마감 시간을 초과했습니다. 범위를 좁혀 다시 시도하세요. |
internal_error | 500 | 예상치 못한 서버 오류입니다. 다시 시도하고, 계속되면 신고해 주세요. |
속도 제한 및 한도
| 가드 | 값 |
|---|---|
| 속도 제한 | 워크스페이스당 분당 60회 요청, 모든 인스턴스에서 공유되는 슬라이딩 윈도우입니다. |
| 동시성 | 워크스페이스당 서버마다 동시 요청 3개, 내보내기 스트림은 워크스페이스당 서버마다 1개. |
| 잔액 폴링 | /balance: 워크스페이스당 분당 60회, 동시 2건까지이며 다른 엔드포인트와 별도로 계산합니다. |
| 범위 한도 | /usage: hour 단위 31일, day 단위 366일. /summary: 최대 366일. /records 및 내보내기: 최대 31일. |
| 페이지 한도 | /usage는 페이지당 최대 5,000행을 반환하며, /records는 최대 1,000행입니다. 내보내기는 2,000행 단위로 스트리밍하며 1,000,000행에서 멈춥니다 - cursor로 재개할 수 있습니다. 기록 id 2,000,000개를 훑은 뒤에도 멈추며(stopped_because=scan_cap), 데이터베이스에 맞춰 속도를 늦춥니다. cursor로 이어서 받으세요. |
| 캐싱 | 모든 응답에는 Cache-Control: no-store가 포함됩니다. |
절대 반환되지 않는 항목: channel_id, upstream_request_id, usage_raw, other, content, ip_address, user_agent, cost_detail.
시간 공백 피하기
요청은 완료된 후 비동기적으로 로그에 기록됩니다 (보통 몇 초, 백로그가 있으면 더 오래 걸립니다). /usage와 /summary가 읽어오는 시간별 롤업은 배치 단위로 적재됩니다. 가장 최근 몇 시간은 항상 약간 불완전합니다. /usage와 /summary는 마지막 수집 이후 기록된 요청도 더합니다(meta.live_tail이 true). 롤업이 너무 뒤처지면 더하지 않으며(meta.live_tail이 false), 최근 버킷은 불완전합니다.
data_complete_until은 다음 두 값 중 더 이른 값입니다: 시간별 롤업의 워터마크에서 현재 파이프라인 지연을 뺀 값, 또는 아직 롤업되지 않은 가장 오래된 요청 - 여기서 30분의 안전 마진을 뺀 뒤 시간 단위로 내림합니다.
확정된 버킷은 안정적이지만 한 가지 예외가 있습니다: 일일 정합성 검사 작업이 오차를 발견하면 48시간 이내에는 해당 시간대의 데이터를 여전히 수정할 수 있습니다. /records는 항상 요청의 정확한 수치에 대한 출처입니다.
모든 /usage 행에는 final 플래그가 있습니다: true 는 해당 버킷의 종료 시각이 data_complete_until (/freshness의 값) 이전이거나 같을 때 성립합니다. 확정된 버킷은 다시는 바뀌지 않습니다.
- 집계 데이터: (bucket_start, api_key_id, model)을 기준으로 각 행을 자체 저장소에 upsert하고, 여전히 final: false인 버킷은 다시 가져오세요. 새 합계에서 이전 합계를 빼서 델타를 구하는 방식은 절대 사용하지 마세요.
- 원시 행: cursor로 동기화하세요. 절대 시간으로 동기화하지 마세요. next_cursor를 보관하고 다음 호출에서 거기서부터 재개하며, 재전송된 페이지가 같은 행을 다시 전달할 경우를 대비해 수신한 행을 record_id(행마다 안정적인 불투명 id)로 중복 제거하세요. 기록된 지 약 5분이 안 된 행은 보류됩니다 - safe_until은 가장 최근에 기록된 행의 시각에서 5분을 뺀 값입니다 - 그래서 아직 커밋 중인 행이 이미 내준 커서 위치보다 앞서 나타나는 일이 없으며, 공백에 빠지는 데이터도 없습니다. 요청 수를 셀 때는 request_id 기준으로 중복을 제거해 세세요.
- 제한된 창: cursor로 동기화하는 대신 start/end 범위로 조회한다면, window_final이 true일 때만 완전하다고 신뢰하세요 (end를 지정했고 그것이 safe_until 이전이거나 같을 때만 true입니다). window_final이 false라면 같은 범위로 나중에 다시 호출하거나 cursor 동기화로 전환하세요. 기간의 시작은 safe_until 이전이어야 합니다. 그보다 늦은 시작은 Retry-After 헤더와 함께 400 start_too_recent를 반환합니다.
권장 연동 방법
- 야간 인보이스 처리: 하루에 한 번 granularity=day로 /usage를 호출하고, 지난번 실행에서 여전히 final: false였던 버킷은 다시 가져오세요.
- 요청 단위 상세: 한 시간마다 저장해 둔 마지막 next_cursor를 cursor로 설정해 /records를 호출하고, has_more가 true인 동안 계속 페이지네이션하며, 수신한 행은 record_id로 중복 제거하세요.
- 대시보드: 표시 중인 범위에는 /records를 직접 집계하지 말고 /summary를 호출하세요 - 이미 이상 목록을 포함하고 있습니다.
청구 데이터를 읽는 대신 일반 API 키를 프로그래밍 방식으로 발급하거나 관리해야 하나요? Provisioning Keys를 참고하세요.