> ## Documentation Index
> Fetch the complete documentation index at: https://docs.otp.id/llms.txt
> Use this file to discover all available pages before exploring further.

# OTP.ID API Response Format: JSON Envelope Structure Guide

> Every OTP.ID API response uses a consistent JSON envelope with success, data, and error fields. Learn the structure and how to parse responses.

Every response from the OTP.ID v3 API — whether the request succeeded or failed — uses the same top-level JSON envelope. Understanding this format lets you write a single, consistent response-handling layer in your application rather than special-casing each endpoint.

## Envelope structure

```json theme={null}
{
  "success": boolean,
  "data": object | null,
  "error": object | null
}
```

The two rules that always hold:

* When `success` is `true`, `data` contains the endpoint-specific response payload and `error` is `null`.
* When `success` is `false`, `data` is `null` and `error` contains a machine-readable `code` and a human-readable `message`.

<ResponseField name="success" type="boolean" required>
  Whether the request was processed successfully. Use this as your primary branch condition — check `success` before reading `data` or `error`.
</ResponseField>

<ResponseField name="data" type="object | null">
  The response payload when `success` is `true`. The shape of this object varies by endpoint. It is always `null` when `success` is `false`.
</ResponseField>

<ResponseField name="error" type="object | null">
  The error details when `success` is `false`. Always `null` when `success` is `true`.

  <Expandable title="error object fields">
    <ResponseField name="error.code" type="string">
      A machine-readable error code such as `UNAUTHORIZED` or `INSUFFICIENT_BALANCE`. **Use this field for all branching logic in your code.** The full list of codes is in the [Error Codes reference](/reference/error-codes).
    </ResponseField>

    <ResponseField name="error.message" type="string">
      A human-readable description of the error. This text is intended for logs and debugging — do **not** use it for programmatic branching because the wording may change between API versions.
    </ResponseField>

    <ResponseField name="error.details" type="object" optional>
      Additional structured context. Only present for specific errors. Currently only returned for `DUPLICATE_EXTERNAL_ID`, where `details.existing_otp_id` gives you the `otp_id` of the original transaction.
    </ResponseField>
  </Expandable>
</ResponseField>

<Warning>
  Never branch on `error.message` text. Always use `error.code`. The message is a localised, human-readable string that may change without notice; the code is a stable contract.
</Warning>

***

## Success response

A successful response sets `success: true`, populates `data` with the endpoint result, and sets `error` to `null`.

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "status": "sent",
    "channel": "whatsapp",
    "number": "6281234567890",
    "price": 350,
    "last_balance": 99650,
    "expires_at": "2026-08-07 10:05:00"
  },
  "error": null
}
```

The `data` fields shown above are typical of a `POST /v3/request` or `POST /v3/send` response. Each endpoint documents its own `data` shape in the [API Reference](/api-reference/request-otp).

***

## Error response

A failed response sets `success: false`, sets `data` to `null`, and populates `error` with a code and message.

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Saldo tidak cukup"
  }
}
```

***

## Error response with details

A small number of error codes include an `error.details` object with additional structured data. Currently, only `DUPLICATE_EXTERNAL_ID` returns `details`.

```json theme={null}
{
  "success": false,
  "data": null,
  "error": {
    "code": "DUPLICATE_EXTERNAL_ID",
    "message": "external_id sudah pernah dipakai dengan parameter berbeda",
    "details": {
      "existing_otp_id": "OTP20260807ABCD000001"
    }
  }
}
```

When you receive `DUPLICATE_EXTERNAL_ID`, read `error.details.existing_otp_id` and call `GET /v3/otp/{otp_id}` with that value to retrieve the status of the original transaction. See the [Idempotency guide](/guides/idempotency) for the full retry pattern.

***

## Parsing the envelope

Structure your response-handling code around the `success` flag first, then delegate to error-specific logic based on `error.code`.

```javascript theme={null}
async function callOtpApi(endpoint, body) {
  const res = await fetch(`https://api.otp.id${endpoint}`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });

  const envelope = await res.json();

  if (envelope.success) {
    return envelope.data; // shape varies by endpoint
  }

  // Always switch on error.code — never on error.message
  switch (envelope.error.code) {
    case "INSUFFICIENT_BALANCE":
      throw new InsufficientBalanceError();
    case "DUPLICATE_EXTERNAL_ID":
      const existingId = envelope.error.details?.existing_otp_id;
      return lookupExistingOtp(existingId);
    case "OTP_EXPIRED":
    case "TOO_MANY_ATTEMPTS":
    case "ALREADY_USED":
      throw new TerminalOtpError(envelope.error.code);
    case "RATE_LIMITED":
      throw new RateLimitedError();
    default:
      throw new OtpApiError(envelope.error.code, envelope.error.message);
  }
}
```

***

## Special cases

### Wrong OTP code is not an error

When a user submits an incorrect OTP code to `POST /v3/verify`, the API does **not** return an error response. Instead, it returns HTTP 200 with `success: true` and `data.verified: false`. Check `data.verified` to determine whether verification passed.

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "verified": false
  },
  "error": null
}
```

<Warning>
  Do not treat `data.verified: false` as an error. It is a normal, successful API response telling you the submitted code was wrong. Only look for `success: false` to detect actual API errors.
</Warning>

### Verify success response

A successful verification returns `data.verified: true` along with a `verified_at` timestamp.

```json theme={null}
{
  "success": true,
  "data": {
    "otp_id": "OTP20260807ABCD000001",
    "verified": true,
    "verified_at": "2026-08-07 10:03:42"
  },
  "error": null
}
```

***

## Data formats

### Timestamps

All timestamp fields (`expires_at`, `verified_at`) use the format `YYYY-MM-DD HH:MM:SS` in **WIB (Western Indonesian Time, UTC+7)**. When storing or comparing timestamps in your own system, convert to UTC or your local timezone as needed.

```
"expires_at": "2026-08-07 10:05:00"   // WIB = 2026-08-07 03:05:00 UTC
"verified_at": "2026-08-07 10:03:42"  // WIB = 2026-08-07 03:03:42 UTC
```

<Note>
  WIB is UTC+7. To convert a WIB timestamp to UTC, subtract 7 hours.
</Note>

### OTP ID format

Every OTP transaction is assigned a unique `otp_id` with the following structure:

```
OTP  +  YYYYMMDD  +  XXXX  +  NNNNNN
 ↑         ↑          ↑         ↑
prefix   date    4 random    6-digit
               uppercase   daily counter
               letters
```

**Example:** `OTP20260807ABCD000001`

* `OTP` — fixed prefix
* `20260807` — date the OTP was created (YYYYMMDD)
* `ABCD` — 4 random uppercase letters
* `000001` — zero-padded daily counter, resets each day

Store the full `otp_id` string. Use it as the path parameter for `GET /v3/otp/{otp_id}` and as the body parameter for `POST /v3/verify`.
