내 코드로 직접 연결하기

직접 호스팅하는 앱을 DontCode 프로젝트에 연결하세요: 인증, 데이터베이스, 파일 스토리지, 결제, 이메일, 공유 키-값 저장소, 실시간 기능을 프로젝트 API 키 하나로 일반 HTTP를 통해 사용합니다.

10분 분량
v1 API
DontCode Backend 소개

개요

퍼블릭 API는 직접 호스팅하는 앱이 DontCode의 플랫폼 서비스(인증, 데이터베이스, 파일 스토리지, 결제, 트랜잭션 이메일, 공유 키-값 저장소, 실시간 pub/sub)를 프로젝트 API 키로 일반 HTTP를 통해 사용할 수 있게 합니다. dontcode SDK와 동일한 와이어 프로토콜을 사용하므로, 앱은 플랫폼 측 변경 없이 해당 SDK와 원시 HTTP 사이를 오갈 수 있습니다.

계약은 안정적입니다: 필드, 상태 코드, 형태는 추가 방식으로만 변경됩니다. 서비스가 내부적으로 어떻게 동작하는지는 계약의 일부가 아닙니다.

DontCode가 처음이신가요? 전체 제품 문서는 여기에 있습니다: dontcode.co/docs

요청 인증

모든 요청은 프로젝트 API 키를 Bearer 토큰으로 전달합니다:

Authorization: Bearer dc_<key>

키는 프로젝트 단위로 관리됩니다 (콘솔 → 프로젝트 → API 키). 평문은 생성 시 한 번만 표시되며 해시만 저장됩니다. 폐기는 즉시 적용됩니다.

두 개의 환경 변수로 앱을 게이트웨이에 연결하세요: 키 생성 시 표시되는 패널에서 그대로 복사하면 됩니다:

DONTCODE_API_URL=https://backend.dontcode.co
DONTCODE_API_KEY=dc_…

속도 제한은 프로젝트별, 네임스페이스별로 적용됩니다. 실시간 호출이 아무리 많아도 데이터베이스나 인증 호출이 밀리지 않습니다. 한도는 키가 아니라 프로젝트를 따릅니다. 키를 새로 발급하거나 교체해도 한도가 늘어나지 않습니다.

한도기본값
/api/v1/realtime분당 1200회
/api/v1/cache분당 1000회
/api/v1/db 읽기분당 900회
/api/v1/db 쓰기분당 300회
/api/v1/auth분당 300회
/api/v1/payments분당 120회
/api/v1/storage분당 120회
/api/v1/notifications분당 60회
/api/v1/db/migrate분당 10회

/api/v1/db는 읽기와 쓰기로 나누어 한도를 적용합니다. find, findMany, findFirst, findOne, count는 읽기 한도(db/read)를, insert, update, delete는 쓰기 한도(db/write)를 사용합니다. 두 한도는 별개의 카운터이므로, 폴링 루프가 읽기 한도를 모두 써도 쓰기는 계속 처리됩니다. 읽기는 비용이 낮고 다시 시도하면 되지만, 실패한 쓰기는 데이터 손실이기 때문입니다.

모든 응답에 RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, RateLimit-Scope가 포함되므로 차단되기 전에 호출 속도를 조절할 수 있습니다. 하나의 경로가 둘 이상의 한도를 사용할 수 있으므로, 경로로 추측하지 말고 RateLimit-Scope를 읽으세요. 한도를 초과하면 429 { error, rate_limit: true, timeleft, scope }를 반환하며, scope는 소진된 한도의 이름을 나타냅니다. 응답에는 timeleft와 같은 초 단위 값을 담은 Retry-After 헤더도 함께 전달됩니다.

TypeScript SDK: dontcode

HTTP를 직접 작성하고 싶지 않으신가요? dontcode는 이 페이지의 모든 엔드포인트를 감싸는 얇은 타입드 래퍼입니다. 동일한 와이어 프로토콜과 동일한 형태를 사용하므로 언제든 원시 HTTP로 돌아갈 수 있습니다. fetch가 동작하는 모든 환경(Node 18+, 브라우저)에서 실행되며 런타임 의존성이 없습니다.

pnpm add dontcode

클라이언트를 생성하세요. apiKey는 DONTCODE_API_KEY로, 게이트웨이 주소는 DONTCODE_API_URL로 기본 설정됩니다. 위의 두 변수와 동일하므로 값이 설정되어 있으면 dontcode()에 인자가 필요 없습니다. 어느 곳에도 키가 없으면 게이트웨이가 401 Missing API key를 반환합니다.

import { dontcode } from 'dontcode'

const client = dontcode() // reads DONTCODE_API_KEY / DONTCODE_API_URL

// Auth: branch on verification_required / mfa_required, like the raw API
await client.auth.signup({ email, password, role: 'editor' })
const login = await client.auth.login({ email, password })
const { user } = await client.auth.me({ accessToken })

// Database: db.<table> or db('table'); migrations via db.migrate
const rows = await client.db.profiles.find({ where: { ownerId }, limit: 20 })
await client.db.profiles.insert({ name })
await client.db.migrate({ sql })

// Storage: public / private buckets
await client.storage.private.upload('docs/a.pdf', bytes, 'application/pdf')
const { url } = await client.storage.public.getUrl('img/logo.png')

모든 메서드는 아래 라우트와 일대일로 매핑됩니다: client.auth.*는 /api/v1/auth/*로, client.db.<table>와 client.db.migrate는 /api/v1/db로, client.storage.public|private는 /api/v1/storage로 연결됩니다. 로그인된 사용자 호출(me, mfa.* 메서드)에는 최종 사용자 토큰을 accessToken으로 전달하면 X-Access-Token에 실립니다.

2xx가 아닌 응답은 status, code, 전체 body를 담은 DontCodeError를 던집니다. “한 단계 더” 인증 상태(verification_required, mfa_required)는 오류가 아니라 정상 응답이므로 반환된 값으로 분기하세요.

이제 모든 SDK 요청에 타임아웃이 적용되므로(기본 10초, timeoutMs로 변경) 게이트웨이가 느려도 멈추지 않고 빠르게 실패합니다. 매 요청마다 왕복 없이 내비게이션 사이에서 로그인 상태를 유지하려면 아래 세션을 참고하세요.

데이터베이스: POST /api/v1/db

구조화된 쿼리만 허용됩니다: 원시 SQL은 여기서 절대 받지 않습니다. 본문은 구조화된 쿼리 프로토콜입니다:

{ "operation": "find", "tableName": "maps", "options": { … } }
operationoptions반환 (data)
find / findManywhere, select, orderBy, limit, offset행 (호출당 최대 1000개)
findOne / findFirstwhere, select, orderBy행 또는 null
insertdata{ id }
updatewhere (필수), data{ count }
deletewhere (필수){ count }
countwhere숫자

where 절: 직접 동등 비교, null(IS NULL), 배열(IN), 연산자 객체(equals, not, gt, gte, lt, lte, in, notIn, contains, startsWith, endsWith, 대소문자 무시는 mode: "insensitive"), 그리고 AND / OR / NOT 조합. 모든 값은 서버에서 파라미터화됩니다.

오류는 { error: string }입니다. 고유/외래 키 충돌은 409를 반환하며, 이는 지원되는 멱등 패턴입니다: insert 후 409가 오면 행이 이미 존재하는 것으로 처리(또는 다시 읽기)하세요. include(관계)는 아직 퍼블릭 API에서 제공되지 않습니다.

마이그레이션: POST /api/v1/db/migrate

{ "sql": "CREATE TABLE IF NOT EXISTS profiles (…);" }

외부에서 프로젝트 데이터베이스로 DDL이 들어오는 유일한 통로입니다. 구문은 콘솔이 사용하는 동일한 파이프라인으로 검증·정제됩니다: 단일 스키마 격리, 역할/권한 관리 불가, 확장 불가, 플랫폼 관리 users 테이블 보호. { success, executedStatements, warnings } 또는 400 { success: false, error }를 반환합니다.

인증: POST /api/v1/auth/{endpoint}

DontCode 인증을 안정적인 요청/응답 형태로 프록시합니다. 엔드포인트: signup, login, me, verify-email, forgot-password, reset-password, mfa/challenge, mfa/enroll, mfa/enroll/confirm, mfa/disable.

두 개의 토큰, 절대 섞지 않기

  • API 키(Authorization: Bearer dc_…)는 *프로젝트*를 식별합니다. 아래 인증 호출을 포함한 모든 요청에 들어갑니다.
  • 최종 사용자 액세스 토큰은 X-Access-Token 헤더로 전달합니다. 로그인된 사용자로 동작하는 모든 호출(me, mfa/enroll, mfa/enroll/confirm, mfa/disable)에 보내세요. 세션은 앱이 소유합니다; 토큰은 httpOnly 쿠키에 저장하세요.
아래 두 가지 동작은 API 플래그가 아니라 프로젝트 설정이며, 소유자가 언제든 바꿀 수 있으므로 앱은 두 상태를 모두 처리해야 합니다: 이메일 인증과 MFA. signup이 토큰을 반환한다고 가정하지 말고, login이 한 단계라고 가정하지 마세요.

회원가입: signup

{ email, password, name?, role?, accept_as_typed? }. 응답은 프로젝트가 이메일 인증을 요구하는지에 따라 달라집니다:

// 인증 ON: 아직 토큰 없음; 사용자가 코드를 입력해야 함
{ "success": true, "userId": "…", "verified": false,
  "verification_required": true,
  "message": "User created. Please verify your email." }

// 인증 OFF: 계정 즉시 사용 가능; 바로 로그인
{ "success": true, "userId": "…", "verified": true,
  "verification_required": false }

verification_required가 true이면 앱은 코드를 받아야 합니다. DontCode가 신규 사용자에게 6자리 코드(예: 174829)를 이메일로 보냅니다. 코드 입력 폼을 표시하고 verify-email로 POST하세요; signup만으로는 흐름이 끝나지 않습니다.

DontCode는 계정을 만들기 전에 주소를 확인합니다. 메일을 받을 수 없는 주소나 일회용 메일함은 400 { code: "UndeliverableEmailException" }로 실패합니다. 정상 도메인의 오타로 보이는 주소(gmai.com)는 422 { code: "EmailTypoSuspected" }로 잠시 멈춥니다. 두 경우 모두 did_you_mean이 올 수 있으니 한 번에 고칠 수 있게 보여 주세요. 멈춘 주소를 입력한 그대로 쓰려면 accept_as_typed: true를 붙여 signup을 다시 보내세요. 메일을 받을 수 없는 주소는 이 값으로도 통과하지 않습니다.

// 오타로 보임: 아직 아무것도 만들지 않음
422 { "success": false, "code": "EmailTypoSuspected",
      "did_you_mean": "jane@gmail.com" }

// 메일 수신 불가 / 일회용 메일함
400 { "success": false, "code": "UndeliverableEmailException",
      "did_you_mean": "jane@naver.com" }   // 고칠 주소가 있을 때

이메일 코드 확인: verify-email

{ code }: 이메일의 6자리 코드. (email은 받지만 무시됩니다. 코드만으로 사용자를 식별합니다.)

{ "success": true, "message": "Email verified successfully" }  // → 이제 login 호출
// 잘못/만료된 코드 → 400 { "success": false, "error": "…" }

퍼블릭 API에는 아직 별도의 “재전송”이 없습니다; 미인증 이메일로 signup을 다시 실행하면 코드가 재전송됩니다.

로그인: login

{ email, password }. 두 가지 성공 응답이 있습니다; mfa_required로 분기하세요:

// MFA 없음 → 세션 획득
{ "success": true, "userId": "…", "mfa_offered": false,
  "mfa_enabled": false,
  "tokens": { "AccessToken": "…", "ExpiresIn": 3600 } }

// MFA 활성 → 아직 로그인 아님; 단기 챌린지 보유
{ "success": true, "userId": "…", "mfa_required": true,
  "challenge_token": "…", "challenge_expires_in": 300 }

이메일 인증 전 로그인 시도는 403 { code: "EmailNotVerified" }로 실패합니다; 사용자를 verify-email 단계로 되돌리세요.

MFA 로그인 완료: mfa/challenge

login이 mfa_required를 반환했다면, 챌린지와 사용자의 인증 앱 코드를 실제 토큰으로 교환하세요: { challenge_token, code } (또는 복구 코드를 사용하려면 { challenge_token, recovery_code }).

{ "success": true, "tokens": { "AccessToken": "…", "ExpiresIn": 3600 } }
// 만료 → 401 { "code": "ChallengeExpired" }  → login 다시 시작

MFA 등록 / 해제 (로그인된 사용자; X-Access-Token 전송)

MFA는 사용자별 선택 사항입니다. 프로젝트의 MFA-제공 설정은 등록을 가능하게 할 뿐, 누구에게도 강제하지 않습니다. 사용자는 mfa/enroll/confirm을 완료한 후에만 mfa_required 로그인 분기를 보게 됩니다. 소유자가 프로젝트에 MFA를 활성화하지 않았다면 mfa/enroll은 403 { code: "MfaNotOffered" }를 반환합니다.

  1. mfa/enroll (본문 없음) → { success, secret, otpauth_url }. otpauth_url을 QR 코드로 렌더링(또는 secret 표시)하여 사용자의 인증 앱에 등록하게 하세요. 등록은 보류 상태입니다; mfa_enabled는 아직 바뀌지 않습니다.
  2. mfa/enroll/confirm { code }: 인증 앱의 첫 코드. 성공 시 → { success, recovery_codes: [...] }. 복구 코드를 한 번만 보여주고 저장하도록 안내하세요; 다시 반환되지 않습니다.
  3. mfa/disable { code } 또는 { recovery_code }: MFA를 끄기 전에 두 번째 인증 요소 소유를 증명합니다 → { success, message }.

현재 사용자: me

본문 없음. 사용자 토큰을 X-Access-Token으로 보내세요 → { user: { id, email, role, claims } }, 토큰이 없거나 만료되면 401 { user: null }.

UI에 연결하기

앱은 올바른 인증 플로우에 필요한 2단계 회원가입 및 로그인 화면을 직접 만드는 것입니다. 이 화면들이 따르는 규칙: 한 번의 제출이 “완료” 상태 또는 “한 단계 더” 상태를 반환할 수 있습니다. 응답에 따라 분기하고, 한 번의 왕복으로 끝난다고 가정하지 마세요. 이를 잘못 처리하는 것이 BYOC의 가장 흔한 버그입니다: 회원가입 폼이 코드 단계를 빠뜨리고, 로그인 폼이 MFA 단계를 빠뜨립니다.

이 호출 후응답 신호UI가 해야 할 일
signupverification_required: true6자리 코드 폼으로 전환 → verify-email, 그 다음 login
signupverified: true계정 사용 가능 → 사용자를 login으로
logintokens 존재세션 저장; 완료
loginmfa_required: true (+ challenge_token)인증 앱 코드 폼으로 전환 → mfa/challenge
loginmfa_offered: true, mfa_enabled: false선택적으로 “2단계 인증 켜기” 유도 → 등록 흐름
me401 / user: null로그아웃 상태로 처리

MFA를 필수로 만들기: 서버는 MFA를 강제하지 않습니다; login은 이미 등록한 사용자에게만 mfa_required를 반환합니다. 모두에게 요구하려면 로그인 후 앱에서 게이트하세요: mfa_offered: true이고 mfa_enabled: false이면 진행 전에 사용자를 등록 흐름으로 보내세요.

세션: 멈춤 없이 로그인 유지하기

이들은 새로운 엔드포인트가 아니라 SDK 편의 기능이며, 와이어 계약은 그대로입니다. 세션은 여전히 앱이 저장한 액세스 토큰일 뿐입니다. 이 기능은 한 가지 함정을 해결합니다: 매 내비게이션마다 me를 호출해 인증을 확인하면 핫 패스에서 네트워크 왕복이 발생하고, 게이트웨이가 느리면 그 멈춘 요청이 “로그아웃됨”으로 읽혀 사용자를 로그인으로 되돌립니다. 이 헬퍼로 라우트는 로컬에서 게이트하고, 꼭 필요할 때만 검증하세요. 프레임워크에 구애받지 않습니다: 토큰이나 쿠키 헤더를 받아 일반 데이터를 반환하므로 Next 미들웨어, SvelteKit hooks, Express, 워커 어디든 어댑터 없이 들어갑니다.

토큰 저장

serializeSessionCookie(token, options?)는 Set-Cookie 값을 반환하고, clearSessionCookie()는 로그아웃용 값을, readSessionToken(cookieHeader)는 토큰을 다시 읽어옵니다. 문자열을 적용하는 것은 프레임워크의 몫입니다. 기본값은 DontCode 자체 앱의 저장 방식과 동일합니다: httpOnly(클라이언트 JavaScript가 토큰을 읽지 못함), Secure, SameSite=Lax, 경로 /, 이름 dc_access_token, 7일 수명. 교차 사이트 구성에서는 sameSite: "none"이 필요하며 이는 Secure를 강제합니다.

import { serializeSessionCookie, clearSessionCookie } from 'dontcode'

// after a successful login (tokens.ExpiresIn keeps cookie + token in lockstep)
headers.append('Set-Cookie', serializeSessionCookie(login.tokens.AccessToken, {
  maxAge: login.tokens.ExpiresIn,
}))

// on logout
headers.append('Set-Cookie', clearSessionCookie())

getSession으로 라우트 게이트하기

client.auth.getSession({ accessToken, mode })(또는 쿠키를 대신 읽어주는 sessionFromCookies(cookieHeader, { mode }))는 토큰을 { status, user, verified, expiresAt }로 해석합니다. status는 active, expired, anonymous, unavailable 중 하나입니다.

// route guard: instant, no network, runs on every navigation
const session = await client.auth.sessionFromCookies(cookieHeader)
if (session.status !== 'active') return redirect('/login')

// before a sensitive action: confirm against the gateway
const checked = await client.auth.getSession({ accessToken, mode: 'verified' })
if (checked.status !== 'active') return redirect('/login')

optimistic 모드의 동작 방식(그리고 한계)

  • optimistic(기본값)은 토큰의 클레임을 로컬에서 디코딩하고 신뢰합니다. 서명 검증도, 네트워크도 없으며 즉시 응답합니다. 페이지 로드 게이트와 UI 상태 구동에 적합합니다.
  • 서버 측 폐기(전체 로그아웃, 비밀번호 변경)는 토큰 자체의 exp 이전에는 감지하지 못합니다. 토큰은 게이트웨이가 공유하지 않는 비밀 키로 서명되므로 로컬 코드는 토큰의 진위도 증명할 수 없습니다; 오직 게이트웨이만 가능합니다.
  • verified가 권위 있는 판단입니다. me를 한 번 호출하고 ttlMs(기본 60초) 동안 캐시하며 verifyTimeoutMs(기본 5초) 후 타임아웃됩니다. 타임아웃이나 장애 시에는 status: "unavailable"와 함께 optimistic으로 디코딩한 사용자를 반환하므로, 모두를 잠그는 대신 페일 오픈할지 직접 결정할 수 있습니다. 실제 401은 anonymous로 보고됩니다: 사용자가 정말로 로그아웃된 것입니다.
모드네트워크폐기 감지용도
optimistic (기본값)없음아니오, exp까지페이지 로드 게이트, UI 상태
verifiedme, 캐시 + 타임아웃예, ttlMs 이내민감한 작업, 권한 확인
권장 사항: 페이지 로드는 optimistic으로 게이트하고, 민감한 작업(결제, 계정·보안 변경, 관리자 작업) 전에는 verified로 다시 확인하세요. 토큰 수명을 짧게 유지해 optimistic과 실제 상태가 크게 벌어지지 않게 하세요. 반드시 확실해야 하는 권한 결정을 optimistic 클레임만으로 내리지 마세요.

스토리지: /api/v1/storage

{ operation, bucket: "public" | "private", … }로 POST:

operationparams반환
listprefix?{ objects, folders, prefix, truncated, continuationToken }
removepaths[]{ deleted }
movefrom, to{ object }
createFolderpath{ created }
downloadpath{ body (base64), contentType, size } (≤ 8 MB)
getUrlpath (public 버킷만){ url }
getTemporaryUrlpath, expiresIn?{ url, expiresIn }
presignUploadpath, contentType?{ url, key, expiresIn }

multipart/form-data(file, bucket, path, contentType?)로 PUT하면 직접 업로드되며(≤ 100 MB) { object }를 반환합니다. 더 큰 파일: presignUpload 후 반환된 URL로 파일을 PUT하세요. 모든 경로는 자동으로 프로젝트 범위로 한정됩니다.

결제: /api/v1/payments

일회성 결제를 받고 구독을 운영하며, 요금제·기능 카탈로그와 이용 권한 확인을 제공합니다. 이것은 서버 컨트롤 플레인입니다: 서버가 완료된 결제를 검증하고, 구독 수명 주기를 관리하며, 누가 무엇에 대한 권한을 갖는지 조회합니다. 고객이 보는 결제 팝업은 결제 제공자를 대상으로 브라우저에서 실행되며, 게이트웨이는 그 결과를 실제로 반영하기 위해 서버가 호출하는 대상입니다. 금액은 요금제 통화(기본 KRW)의 정수이며, 모든 것은 자동으로 프로젝트 범위로 한정됩니다.

이 엔드포인트에는 프로젝트 API 키만 도달하며, 최종 사용자 토큰은 절대 도달하지 않습니다. 서버가 모든 작업을 조율하고 동작 주체인 userId를 명시적으로 전달합니다. 작업(operation)은 본문 필드가 아니라 경로의 일부이며, SDK의 client.payments.*와 일대일로 매핑됩니다.

일회성 결제

method + pathbody반환
POST /verify{ paymentId, expectedAmount, currency, description?, userId? }{ receipt }
POST /refund{ paymentId, amount?, reason }{ refundedAmount, status, already_refunded }

verify는 고객이 방금 완료한 결제를 기록합니다; 무언가를 부여하기 전에 호출하세요. refund에서 amount를 생략하면 전액 환불이며, 환불은 멱등적입니다.

구독

method + pathbody반환
POST /subscribe{ plan, userId, method, billingKey }{ subscription }
POST /subscribe-reserve{ plan, userId, method }{ subscription_id, storeId, channelKey }
POST /subscribe-confirm{ subscription_id, billing_key }{ subscription }
POST /subscribe-abort{ subscription_id }{}
POST /cancel-subscription{ subscription, atPeriodEnd }{ subscription }
POST /update-subscription-status{ subscription, status }{ subscription }
POST /get-subscription{ userId } 또는 { subscriptionId }{ subscription } 또는 null
POST /list-active-subscriptions{ userId }{ subscriptions }

시작하는 두 가지 방법: 이미 빌링 키를 보유했을 때는 단일 호출 subscribe, 아니면 분할 흐름 subscribe-reserve 후 반환된 구성으로 브라우저에서 제공자 팝업을 열고 subscribe-confirm(팝업이 취소되면 subscribe-abort)을 호출합니다. cancel-subscription은 atPeriodEnd: false가 아니면 기본적으로 소프트 취소(기간 종료까지 접근 유지)입니다.

이용 권한 및 카탈로그

method + pathbody / query반환
POST /has-feature{ userId, featureKey }{ ok }
POST /list-user-features{ userId }{ features }
GET /plans?includeInactive=true{ plans }
POST /plans{ plans }{ plans }
PATCH /plans{ planId, active }{}
DELETE /plans?planId={}
GET /features{ features }
POST /features{ features }{ features }
DELETE /features?featureKey={}
GET /plan-features?planId={ planFeatures }
PUT /plan-features{ planId, features }{}

요금제는 가격이 매겨진 티어, 기능은 이름이 붙은 기능이며, plan-features는 어떤 요금제가 어떤 기능을 부여하는지 매핑합니다(요금제별 한도는 metadata에 들어갑니다). has-feature는 유료 기능의 게이트입니다; 반드시 확실해야 하는 것은 세션과 동일한 규칙으로 서버에서 다시 확인하세요.

관리자

method + pathbody반환
POST /admin/list-subscriptions{ status?, planId?, userId?, limit?, offset? }{ subscriptions, total }
POST /admin/grant-subscription{ userId, planId, periodStart?, periodEnd? }{ subscription }
POST /charge-subscription{ subscription, plan }{ receipt }

grant-subscription은 결제 없이 구독을 무상 부여합니다(자동 갱신 없음). charge-subscription은 주기 외 결제를 트리거합니다; 갱신은 자동으로 실행되므로 거의 필요하지 않습니다. 이 계약을 벗어난 경로는 404입니다.

오류와 해결 방법

결제 호출이 실패하면 status, 안정적인 code, 전체 body를 담은 DontCodeError가 발생합니다. 메시지는 구매자가 읽을 수 있도록 작성되어 있으므로, 모든 실패를 하나의 일반 메시지로 뭉뚱그리지 말고 그대로 표시하세요. 402는 버그가 아닙니다: 보통 설정이 완료되지 않았거나 구매자가 다시 시도해야 함을 의미합니다.

code상태의미해결 방법
PAYMENTS_DISABLED402이 프로젝트의 결제 처리가 아직 켜지지 않았습니다. 아래 항목보다 먼저 확인됩니다.코드에서 바꿀 것은 없습니다: 프로젝트에 결제가 활성화되면 청구가 동작합니다. 계속되면 hello@dontcode.co로 문의하세요.
BANK_ACCOUNT_REQUIRED402앱에 정산 은행 계좌가 없어 아직 결제를 받을 수 없습니다.앱 소유자: 프로젝트 결제 탭에서 정산 계좌를 등록한 뒤 인증이 완료될 때까지 기다리세요.
BANK_ACCOUNT_PENDING_VERIFICATION402정산 계좌가 아직 인증 중입니다.인증이 끝날 때까지 기다리세요; 결제는 자동으로 재개됩니다.
BANK_ACCOUNT_GRACE_EXPIRED402정산 계좌가 유예 기간을 넘겨 미인증 상태로 남아 있습니다.앱 소유자: 프로젝트 결제 탭에서 계좌를 다시 인증하세요.
BANK_ACCOUNT_SUSPENDED402정산 계좌가 정지되었습니다.지원팀에 문의하세요.
CHARGE_FAILED402결제가 거절되었습니다.구매자: 다시 시도하거나 다른 결제 수단을 사용하세요.
PAYMENT_VERIFICATION_FAILED402완료된 결제를 기록된 금액과 대조해 검증할 수 없습니다.금액과 통화가 요금제와 일치하는지 확인한 뒤 다시 시도하세요.
SUBSCRIPTION_NOT_FOUND404해당 id 또는 사용자에 대한 구독이 없거나, 저장된 결제 수단이 없습니다.확정 또는 청구 전에 구독을 다시 예약(reserve)하세요.
BILLING_KEY_MISSING400이 작업에는 저장된 결제 수단이 필요하지만 존재하지 않습니다.reserve 후 confirm 흐름을 실행해 결제 수단을 저장하세요.
RATE_LIMITED429이 프로젝트의 결제 요청이 너무 많습니다.`timeleft` 초만큼 기다린 뒤 다시 시도하세요.
새로 만든 프로젝트는 소유자가 결제 탭에서 정산 계좌를 등록하기 전까지 모든 청구에서 BANK_ACCOUNT_REQUIRED를 반환합니다. 새로 등록한 계좌는 인증 유예 기간 동안 결제를 받을 수 있고, 유예 기간이 끝날 때까지 인증되지 않으면 청구가 BANK_ACCOUNT_PENDING_VERIFICATION을 반환합니다. 이는 실패가 아니라 예상된 동작입니다: 구매자에게 막다른 길을 보여주지 말고, 소유자가 설정을 마칠 수 있도록 메시지를 표시하세요.

캐시: /api/v1/cache

짧게 유지되는 공유 상태를 위한 프로젝트 범위 키-값 저장소입니다. 앱이 어디에서 어떻게 호스팅되든, 모든 인스턴스가 같은 곳을 읽고 씁니다. 서버리스와 다중 인스턴스 환경은 프로세스 메모리를 공유할 수 없으므로, 인스턴스 간 상태는 여기에 둡니다: 요청 제한 카운터, 일회용 코드와 로그인 논스, 웹훅 중복 방지 가드와 단일 실행 잠금, 비회원 장바구니와 다단계 마법사 세션, 그리고 정말 비용이 큰 작업(대규모 집계, 유료 외부 API, AI 호출)의 결과 캐시가 그 예입니다. 값은 앱을 다시 배포해도 유지되며, 같은 키를 쓰는 웹 앱과 워커, 크론이 함께 공유합니다.

메서드 + 경로본문 / 쿼리반환
GET /kv/{key}{ value } 또는 404
PUT /kv/{key}본문 = 원시 JSON 값; ?ttl=(초), ?nx=true{ ok, set }
DELETE /kv/{key}{ ok, deleted }
POST /expire/{key}{ ttl } (초, null이면 해제){ ok, applied }
POST /hset/{key}{ fields: { … } }{ ok, written }
GET /hgetall/{key}{ value } 또는 404
POST /sadd/{key}{ members: string[] }{ ok, added }
GET /smembers/{key}{ value } 또는 404
POST /srem/{key}{ members: string[] }{ ok, removed }

ttl은 초 단위입니다. PUT의 ?nx=true는 키가 없을 때만 저장하고 이미 있으면 { set: false }를 반환합니다; 이 플래그 하나가 잠금, 단 한 번만 실행되는 작업, 웹훅 중복 제거의 기본 재료입니다. 없거나 만료된 키를 GET하면 404이며, SDK는 이를 null(세트 읽기는 [])로 변환합니다. 문자열, 해시, 세트 키 공간은 서로 구분됩니다. 키는 자동으로 프로젝트 범위로 격리되므로 접두사를 직접 붙이지 마세요.

이것은 임시 상태이지 데이터베이스가 아닙니다: 값은 제거될 수 있고 영구 보존되지 않으므로, 기준 데이터는 /api/v1/db에 두세요. 가벼운 읽기에도 쓰지 마세요; 캐시 왕복 비용은 데이터베이스 쿼리 한 번과 비슷하므로, 이미 빠른 쿼리가 아니라 비용이 큰 작업을 캐시하세요.

실시간: /api/v1/realtime

채팅, 접속 상태(presence), 실시간 업데이트 같은 라이브 기능을 위한 WebSocket 기반 실시간 pub/sub입니다. 게이트웨이는 컨트롤 플레인입니다: 서버가 단기 연결 토큰을 발급하고, 메시지를 발행하고, presence를 조회합니다. 브라우저는 발급된 토큰으로 WebSocket에 연결하며 프로젝트 API 키를 절대 갖지 않습니다. 채널은 자동으로 프로젝트 범위로 격리됩니다.

메서드 + 경로본문반환
POST /token{ channels: string[], identity?, ttl? }{ token, url }
POST /publish{ channel, payload }{ delivered }
GET /channels/{channel}/presence{ presence: [{ id, identity? }] }

서버가 최종 사용자를 인증한 뒤 그 사용자가 쓸 수 있는 채널로 토큰을 발급하고, { token, url }을 브라우저에 전달하면 브라우저가 ${url}?token=${token}으로 연결합니다. 전달은 기록이나 재생이 없는 일회성이므로, 보존해야 하는 내용은 /api/v1/db에 저장하세요.

알림: /api/v1/notifications

사용자에게 트랜잭션 이메일을 보냅니다. 수신자와 발신자 정보는 자동으로 프로젝트 범위로 지정됩니다. 이 엔드포인트에는 프로젝트 API 키만 접근할 수 있고 최종 사용자 토큰은 쓸 수 없습니다: 누구에게 알릴지는 서버가 정합니다. SDK에서는 client.notifications.email.send에 대응합니다.

메서드 + 경로본문반환값
POST /email{ to, subject, markdownText }{ success, messageId? } 또는 { success: false, error }
  • to는 수신자 이메일 하나 또는 이메일 배열입니다.
  • markdownText가 본문 전체이며 GitHub 방식 Markdown으로 씁니다. html, body, text 필드나 템플릿은 없습니다: 보낸 Markdown이 곧 이메일입니다. 내용은 서버에서 정리됩니다.
  • 전달되었다고 가정하기 전에 success를 확인하세요.

현재는 email만 있습니다. 경로의 마지막 부분이 채널이므로 이후 채널은 이 계약을 바꾸지 않고 추가되며, 그 밖의 채널은 404입니다.

내 앱 연결하기

  1. 일반 DontCode 콘솔 흐름으로 프로젝트를 생성하세요: 다른 프로젝트와 동일하게 프로젝트 행, 데이터베이스, 인증 풀이 프로비저닝됩니다.
  2. API 키를 발급하세요: 콘솔 → 프로젝트 → API 키.
  3. DONTCODE_API_URL과 DONTCODE_API_KEY로 앱을 설정하세요(키 표시 패널의 환경 변수 블록을 복사).
  4. /api/v1/db/migrate로 앱 스키마를 적용하세요.
  5. 앱은 /api/v1/auth/*로 가입/로그인, /api/v1/db로 읽기/쓰기, /api/v1/storage로 업로드합니다. Postgres 연결 문자열도, 인증 제공자도, 스토리지 자격 증명도 필요 없습니다.